@agentbridge1/cli 0.0.3 → 0.0.5

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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "builtAt": "2026-06-02T01:31:45.533Z",
3
- "gitHead": "184f85c",
4
- "sourceLatestMtime": "2026-06-02T01:19:37.980Z",
5
- "sourceLatestFile": "src/commands/recover.ts"
2
+ "builtAt": "2026-06-03T09:58:38.231Z",
3
+ "gitHead": "6fcc74c",
4
+ "sourceLatestMtime": "2026-06-03T05:08:44.109Z",
5
+ "sourceLatestFile": "src/commands/doctor.ts"
6
6
  }
@@ -10,35 +10,68 @@ function resolveBaseUrl(override) {
10
10
  config_1.DEFAULT_API_BASE_URL);
11
11
  }
12
12
  async function verifyCredentials(projectId, apiKey, apiBaseUrl) {
13
- const url = `${apiBaseUrl}/v1/dev/projects/${projectId}/health`;
14
- let res;
15
- try {
16
- res = await fetch(url, {
17
- method: "GET",
18
- headers: {
19
- Authorization: `Bearer ${apiKey}`,
20
- "Content-Type": "application/json",
21
- },
22
- });
13
+ const summaryUrl = `${apiBaseUrl}/v1/dev/projects/${projectId}/summary`;
14
+ const packetUrl = `${apiBaseUrl}/v1/dev/projects/${projectId}/packet`;
15
+ const headers = {
16
+ Authorization: `Bearer ${apiKey}`,
17
+ "Content-Type": "application/json",
18
+ };
19
+ async function get(url) {
20
+ try {
21
+ return await fetch(url, { method: "GET", headers });
22
+ }
23
+ catch (e) {
24
+ const msg = e instanceof Error ? e.message : String(e);
25
+ throw new errors_1.SafeCliError(`Could not reach AgentBridge server at ${apiBaseUrl}.\nNetwork error: ${msg}\n\nCheck your AGENTBRIDGE_BASE_URL or internet connection.`);
26
+ }
23
27
  }
24
- catch (e) {
25
- const msg = e instanceof Error ? e.message : String(e);
26
- throw new errors_1.SafeCliError(`Could not reach AgentBridge server at ${apiBaseUrl}.\nNetwork error: ${msg}\n\nCheck your AGENTBRIDGE_BASE_URL or internet connection.`);
28
+ async function readBody(res) {
29
+ try {
30
+ return await res.text();
31
+ }
32
+ catch {
33
+ return "";
34
+ }
27
35
  }
28
- if (res.status === 401 || res.status === 403) {
29
- throw new errors_1.SafeCliError(`API key rejected (HTTP ${res.status}).\n\nGet a valid key at https://agentbridge.dev/dashboard and set AGENTBRIDGE_API_KEY.`);
36
+ function isRouteMissing(body) {
37
+ return body.toLowerCase().includes("route not found");
30
38
  }
31
- if (res.status === 404) {
32
- throw new errors_1.SafeCliError(`Project not found on ${apiBaseUrl}. Check project ID or backend URL.`);
39
+ async function throwAuthOrNotFound(res) {
40
+ if (res.status === 401 || res.status === 403) {
41
+ throw new errors_1.SafeCliError(`API key rejected (HTTP ${res.status}).\n\nGet a valid key at https://agentbridge.dev/dashboard and set AGENTBRIDGE_API_KEY.`);
42
+ }
43
+ if (res.status === 404) {
44
+ throw new errors_1.SafeCliError(`Project not found or key has no access on ${apiBaseUrl}.\nCheck project ID, API key, and backend URL.`);
45
+ }
46
+ const text = await readBody(res);
47
+ throw new http_1.CliHttpError(`GET ${summaryUrl} failed (${res.status}): ${text || "<empty>"}`, res.status, text);
33
48
  }
34
- if (!res.ok) {
35
- const text = await res.text();
36
- throw new http_1.CliHttpError(`GET ${url} failed (${res.status}): ${text || "<empty>"}`, res.status, text);
49
+ let res;
50
+ res = await get(summaryUrl);
51
+ if (res.ok) {
52
+ const body = (await res.json());
53
+ return {
54
+ projectName: body.project_name ?? body.projectName ?? projectId,
55
+ agentCount: typeof body.agent_count === "number"
56
+ ? body.agent_count
57
+ : Array.isArray(body.agents)
58
+ ? body.agents.length
59
+ : 0,
60
+ };
37
61
  }
38
- const body = (await res.json());
62
+ const summaryBody = await readBody(res);
63
+ if (!(res.status === 404 && isRouteMissing(summaryBody))) {
64
+ await throwAuthOrNotFound(res);
65
+ }
66
+ // Backward compatibility for servers that haven't shipped /summary yet.
67
+ const packetRes = await get(packetUrl);
68
+ if (!packetRes.ok) {
69
+ await throwAuthOrNotFound(packetRes);
70
+ }
71
+ const packetBody = (await packetRes.json());
39
72
  return {
40
- projectName: body.projectName ?? projectId,
41
- agentCount: Array.isArray(body.agents) ? body.agents.length : 0,
73
+ projectName: packetBody.project_name ?? projectId,
74
+ agentCount: 0,
42
75
  };
43
76
  }
44
77
  async function listAgents(projectId, apiKey, apiBaseUrl) {
@@ -51,6 +84,113 @@ async function listAgents(projectId, apiKey, apiBaseUrl) {
51
84
  const body = (await res.json());
52
85
  return Array.isArray(body.agents) ? body.agents : [];
53
86
  }
87
+ async function resolveExecutionSurfaceFromHello(projectId, apiKey, apiBaseUrl) {
88
+ const url = `${apiBaseUrl}/v1/dev/projects/${projectId}/hello`;
89
+ try {
90
+ const res = await fetch(url, {
91
+ method: "POST",
92
+ headers: {
93
+ Authorization: `Bearer ${apiKey}`,
94
+ "Content-Type": "application/json",
95
+ },
96
+ body: JSON.stringify({ tool_type: "cli" }),
97
+ });
98
+ if (!res.ok) {
99
+ return {
100
+ executionSurfaceId: null,
101
+ identityModel: "unknown",
102
+ status: `http_${res.status}`,
103
+ };
104
+ }
105
+ const body = (await res.json());
106
+ if (body.identity_model !== "work_identity") {
107
+ if (body.identity_model === "legacy") {
108
+ return {
109
+ executionSurfaceId: null,
110
+ identityModel: "legacy",
111
+ status: "legacy_identity",
112
+ };
113
+ }
114
+ return {
115
+ executionSurfaceId: null,
116
+ identityModel: "unknown",
117
+ status: "unknown_identity_model",
118
+ };
119
+ }
120
+ const surfaceId = body.execution_surface?.id?.trim();
121
+ return {
122
+ executionSurfaceId: surfaceId || null,
123
+ identityModel: "work_identity",
124
+ status: surfaceId ? "ok" : "missing_execution_surface",
125
+ };
126
+ }
127
+ catch {
128
+ return {
129
+ executionSurfaceId: null,
130
+ identityModel: "unknown",
131
+ status: "network_error",
132
+ };
133
+ }
134
+ }
135
+ async function bootstrapDefaultConnection(projectId, apiKey, apiBaseUrl) {
136
+ const url = `${apiBaseUrl}/v1/dev/projects/${projectId}/connections/bootstrap-default`;
137
+ try {
138
+ const res = await fetch(url, {
139
+ method: "POST",
140
+ headers: {
141
+ Authorization: `Bearer ${apiKey}`,
142
+ "Content-Type": "application/json",
143
+ },
144
+ body: JSON.stringify({}),
145
+ });
146
+ if (!res.ok)
147
+ return { ok: false, status: res.status, reason: `http_${res.status}` };
148
+ const body = (await res.json());
149
+ const nextApiKey = body.api_key?.trim();
150
+ const executionSurfaceId = body.execution_surface_id?.trim();
151
+ if (!nextApiKey || !executionSurfaceId) {
152
+ return { ok: false, status: 200, reason: "invalid_payload" };
153
+ }
154
+ return { ok: true, apiKey: nextApiKey, executionSurfaceId };
155
+ }
156
+ catch {
157
+ return { ok: false, status: 0, reason: "network_error" };
158
+ }
159
+ }
160
+ async function rotateActiveConnectionKey(projectId, apiKey, apiBaseUrl) {
161
+ const listUrl = `${apiBaseUrl}/v1/dev/projects/${projectId}/connections`;
162
+ try {
163
+ const listRes = await fetch(listUrl, {
164
+ headers: { Authorization: `Bearer ${apiKey}` },
165
+ });
166
+ if (!listRes.ok)
167
+ return { ok: false, reason: `list_http_${listRes.status}` };
168
+ const listBody = (await listRes.json());
169
+ const activeConnection = (listBody.connections ?? []).find((connection) => connection?.id && connection?.status === "active");
170
+ if (!activeConnection?.id)
171
+ return { ok: false, reason: "no_active_connection" };
172
+ const rotateUrl = `${apiBaseUrl}/v1/dev/projects/${projectId}/connections/${activeConnection.id}/rotate-key`;
173
+ const rotateRes = await fetch(rotateUrl, {
174
+ method: "POST",
175
+ headers: {
176
+ Authorization: `Bearer ${apiKey}`,
177
+ "Content-Type": "application/json",
178
+ },
179
+ body: JSON.stringify({}),
180
+ });
181
+ if (!rotateRes.ok)
182
+ return { ok: false, reason: `rotate_http_${rotateRes.status}` };
183
+ const rotateBody = (await rotateRes.json());
184
+ const rotatedApiKey = rotateBody.api_key?.trim();
185
+ const executionSurfaceId = rotateBody.connection?.execution_surface?.id?.trim();
186
+ if (!rotatedApiKey || !executionSurfaceId)
187
+ return { ok: false, reason: "invalid_payload" };
188
+ return { ok: true, apiKey: rotatedApiKey, executionSurfaceId };
189
+ }
190
+ catch {
191
+ return { ok: false, reason: "network_error" };
192
+ }
193
+ }
54
194
  async function runConnect(options = {}) {
55
195
  process.stdout.write("AgentBridge connect\n");
56
196
  process.stdout.write("─────────────────────────────────────────\n");
@@ -109,13 +249,86 @@ async function runConnect(options = {}) {
109
249
  throw err;
110
250
  }
111
251
  process.stdout.write("OK\n");
252
+ let effectiveApiKey = apiKey;
253
+ const diagnostics = {
254
+ helloIdentityModel: "unknown",
255
+ helloStatus: "not_attempted",
256
+ bootstrapStatus: "not_attempted",
257
+ rotateStatus: "not_attempted",
258
+ };
259
+ const helloResolution = await resolveExecutionSurfaceFromHello(projectId, effectiveApiKey, apiBaseUrl);
260
+ diagnostics.helloIdentityModel = helloResolution.identityModel;
261
+ diagnostics.helloStatus = helloResolution.status;
262
+ let executionSurfaceId = helloResolution.executionSurfaceId;
263
+ let connectionUpgraded = false;
264
+ if (!executionSurfaceId) {
265
+ diagnostics.bootstrapStatus = "attempted";
266
+ const bootstrapAttempt = await bootstrapDefaultConnection(projectId, effectiveApiKey, apiBaseUrl);
267
+ if (bootstrapAttempt.ok) {
268
+ effectiveApiKey = bootstrapAttempt.apiKey;
269
+ executionSurfaceId = bootstrapAttempt.executionSurfaceId;
270
+ diagnostics.bootstrapStatus = "ok";
271
+ diagnostics.rotateStatus = "skipped";
272
+ connectionUpgraded = true;
273
+ }
274
+ else if (bootstrapAttempt.status === 409) {
275
+ diagnostics.bootstrapStatus = "http_409_existing_connection";
276
+ const rotated = await rotateActiveConnectionKey(projectId, effectiveApiKey, apiBaseUrl);
277
+ if (rotated.ok) {
278
+ diagnostics.rotateStatus = "ok";
279
+ effectiveApiKey = rotated.apiKey;
280
+ executionSurfaceId = rotated.executionSurfaceId;
281
+ connectionUpgraded = true;
282
+ }
283
+ else {
284
+ diagnostics.rotateStatus = rotated.reason;
285
+ }
286
+ }
287
+ else {
288
+ diagnostics.bootstrapStatus = bootstrapAttempt.reason;
289
+ diagnostics.rotateStatus = "skipped";
290
+ }
291
+ }
292
+ else {
293
+ diagnostics.bootstrapStatus = "skipped";
294
+ diagnostics.rotateStatus = "skipped";
295
+ }
296
+ if (!executionSurfaceId) {
297
+ (0, config_1.updateConfig)({
298
+ projectId,
299
+ apiKey: effectiveApiKey,
300
+ apiBaseUrl,
301
+ });
302
+ throw new errors_1.SafeCliError([
303
+ "Connection incomplete.",
304
+ "Project access was verified, but AgentBridge could not create or resolve an execution surface.",
305
+ "Tracked work cannot start yet.",
306
+ "",
307
+ `hello identity model: ${diagnostics.helloIdentityModel}`,
308
+ `hello status: ${diagnostics.helloStatus}`,
309
+ `bootstrap-default status: ${diagnostics.bootstrapStatus}`,
310
+ `rotate-key status: ${diagnostics.rotateStatus}`,
311
+ `backend URL: ${apiBaseUrl}`,
312
+ ].join("\n"));
313
+ }
112
314
  // Persist credentials
113
- (0, config_1.updateConfig)({ projectId, apiKey, apiBaseUrl });
315
+ (0, config_1.updateConfig)({
316
+ projectId,
317
+ apiKey: effectiveApiKey,
318
+ apiBaseUrl,
319
+ ...(executionSurfaceId ? { executionSurfaceId } : {}),
320
+ });
114
321
  process.stdout.write("Credentials saved to .agentbridge/config.json\n");
322
+ if (connectionUpgraded) {
323
+ process.stdout.write("Auto-configured an AgentConnection key and execution surface for this project.\n");
324
+ }
325
+ else {
326
+ process.stdout.write(`Execution surface: ${executionSurfaceId}\n`);
327
+ }
115
328
  // Pick or auto-select an agent identity
116
329
  let activeAgentId = options.agentId ?? cfg.activeAgentId;
117
330
  if (!activeAgentId) {
118
- const agents = await listAgents(projectId, apiKey, apiBaseUrl);
331
+ const agents = await listAgents(projectId, effectiveApiKey, apiBaseUrl);
119
332
  if (agents.length === 1 && agents[0]) {
120
333
  activeAgentId = agents[0].id;
121
334
  (0, config_1.updateConfig)({ activeAgentId });
@@ -126,7 +339,7 @@ async function runConnect(options = {}) {
126
339
  for (const a of agents) {
127
340
  process.stdout.write(` ${a.id} ${a.name ?? ""} ${a.role ?? ""}\n`.trimEnd() + "\n");
128
341
  }
129
- process.stdout.write('\nRun `agentbridge use <agent-id>` to select one, then `agentbridge watch` to start.\n');
342
+ process.stdout.write('\nRun `agentbridge use <agent-id>` to select one.\n');
130
343
  }
131
344
  }
132
345
  else {
@@ -136,24 +349,23 @@ async function runConnect(options = {}) {
136
349
  process.stdout.write("\n");
137
350
  process.stdout.write(`Project: ${projectName}\n`);
138
351
  process.stdout.write(`Agents: ${agentCount}\n`);
352
+ process.stdout.write(`Execution surface: ${executionSurfaceId}\n`);
139
353
  process.stdout.write(`Server: ${apiBaseUrl}\n`);
140
354
  process.stdout.write("\n");
141
355
  if (activeAgentId) {
142
- process.stdout.write("✓ Connected. Next steps:\n");
356
+ process.stdout.write("✓ Connected and ready.\n");
143
357
  process.stdout.write([
144
358
  "",
145
- " 1. Start a work session:",
146
- " agentbridge start --summary \"your task\" --scope \"src/\"",
147
- "",
148
- " 2. Run the watcher (keep it running in a terminal):",
149
- " agentbridge watch",
150
- "",
151
- " 3. Code normally — AgentBridge watches in the background.",
152
- " Approvals surface when you cross domain boundaries.",
359
+ "Next:",
360
+ " agentbridge doctor",
361
+ " agentbridge start",
153
362
  "",
154
363
  ].join("\n"));
155
364
  }
156
365
  else {
157
- process.stdout.write("✓ Connected. Run `agentbridge use <agent-id>` then `agentbridge watch`.\n");
366
+ process.stdout.write("✓ Connected.\n");
367
+ process.stdout.write("Run `agentbridge use <agent-id>` and then:\n");
368
+ process.stdout.write(" agentbridge doctor\n");
369
+ process.stdout.write(" agentbridge start\n");
158
370
  }
159
371
  }
@@ -146,7 +146,39 @@ async function checkProjectAccess(ctx) {
146
146
  }
147
147
  async function checkIdentityAccess(ctx) {
148
148
  if (!ctx.configComplete) {
149
- return { status: "skipped", reason: "config_incomplete" };
149
+ return {
150
+ status: "skipped",
151
+ reason: "config_incomplete",
152
+ identityModel: "unknown",
153
+ executionSurfaceId: null,
154
+ startCapable: false,
155
+ };
156
+ }
157
+ const helloUrl = `${ctx.apiBaseUrl}/v1/dev/projects/${ctx.projectId}/hello`;
158
+ let identityModel = "unknown";
159
+ let executionSurfaceId = null;
160
+ try {
161
+ const helloRes = await fetch(helloUrl, {
162
+ method: "POST",
163
+ headers: {
164
+ Authorization: `Bearer ${ctx.apiKey}`,
165
+ "Content-Type": "application/json",
166
+ },
167
+ body: JSON.stringify({ tool_type: "cli" }),
168
+ });
169
+ if (helloRes.ok) {
170
+ const hello = (await helloRes.json());
171
+ if (hello.identity_model === "work_identity") {
172
+ identityModel = "work_identity";
173
+ }
174
+ else if (hello.identity_model === "legacy") {
175
+ identityModel = "legacy";
176
+ }
177
+ executionSurfaceId = hello.execution_surface?.id?.trim() || null;
178
+ }
179
+ }
180
+ catch {
181
+ // Keep default unknown/null and continue with identity resolution checks.
150
182
  }
151
183
  try {
152
184
  const syncCtx = {
@@ -160,14 +192,29 @@ async function checkIdentityAccess(ctx) {
160
192
  (0, server_sync_1.listWorkIdentities)(syncCtx),
161
193
  ]);
162
194
  const callerId = callerPacket?.work_identity?.id ?? null;
163
- if (callerId)
164
- return { status: "ok" };
165
- if (identities.length === 1)
166
- return { status: "ok" };
167
- return { status: "failed", reason: "caller_identity_unresolved" };
195
+ const startCapable = Boolean(executionSurfaceId);
196
+ if (callerId) {
197
+ return { status: "ok", identityModel, executionSurfaceId, startCapable };
198
+ }
199
+ if (identities.length === 1) {
200
+ return { status: "ok", identityModel, executionSurfaceId, startCapable };
201
+ }
202
+ return {
203
+ status: "failed",
204
+ reason: "caller_identity_unresolved",
205
+ identityModel,
206
+ executionSurfaceId,
207
+ startCapable,
208
+ };
168
209
  }
169
210
  catch {
170
- return { status: "failed", reason: "caller_identity_unresolved" };
211
+ return {
212
+ status: "failed",
213
+ reason: "caller_identity_unresolved",
214
+ identityModel,
215
+ executionSurfaceId,
216
+ startCapable: Boolean(executionSurfaceId),
217
+ };
171
218
  }
172
219
  }
173
220
  async function runDoctor(cliRootOverride) {
@@ -234,6 +281,9 @@ async function runDoctor(cliRootOverride) {
234
281
  if (identityAccess.reason) {
235
282
  lines.push(`Caller identity reason: ${identityAccess.reason}`);
236
283
  }
284
+ lines.push(`Caller identity model: ${identityAccess.identityModel}`);
285
+ lines.push(`Execution surface present: ${identityAccess.executionSurfaceId ? "yes" : "no"}`);
286
+ lines.push(`Start capable: ${identityAccess.startCapable ? "yes" : "no"}`);
237
287
  if (projectAccess.status === "failed" && projectAccess.reason) {
238
288
  const view = (0, error_catalog_1.catalogViewForDoctorReason)(projectAccess.reason, projectAccess.suggestedNextAction);
239
289
  lines.push("");
@@ -252,7 +302,9 @@ async function runDoctor(cliRootOverride) {
252
302
  lines.push("Suggested next action: run npm run dogfood:check before external dogfood.");
253
303
  }
254
304
  let productStatus = "not_ready";
255
- if (projectAccess.status === "ok" && identityAccess.status === "ok") {
305
+ if (projectAccess.status === "ok" &&
306
+ identityAccess.status === "ok" &&
307
+ identityAccess.startCapable) {
256
308
  if (hasActiveWork) {
257
309
  productStatus = "active_work_found";
258
310
  }
@@ -281,18 +333,24 @@ async function runDoctor(cliRootOverride) {
281
333
  lines.push("- Next: agentbridge recover");
282
334
  }
283
335
  else {
284
- lines.push("- Connection: required");
336
+ lines.push("- Connection: incomplete");
285
337
  lines.push("- Recovery: pending");
286
338
  if (!projectConfigPresent) {
287
339
  lines.push("- Note: Connection details are missing. Add credentials, then rerun agentbridge doctor.");
288
340
  }
341
+ else if (!identityAccess.startCapable) {
342
+ lines.push("- Reason: execution surface missing.");
343
+ lines.push("- Next: agentbridge connect");
344
+ }
289
345
  else if (identityAccess.status !== "ok") {
290
346
  lines.push("- Note: Caller identity is unresolved. Run agentbridge identity list and set a valid active agent.");
291
347
  }
292
348
  else {
293
349
  lines.push("- Note: Connection check failed. Verify credentials/network, then rerun agentbridge doctor.");
294
350
  }
295
- lines.push("- Next: agentbridge doctor");
351
+ if (identityAccess.startCapable) {
352
+ lines.push("- Next: agentbridge doctor");
353
+ }
296
354
  }
297
355
  process.stdout.write(`${lines.join("\n")}\n`);
298
356
  if (freshness.state === "stale")
@@ -7,6 +7,7 @@ const server_sync_1 = require("../server-sync");
7
7
  const node_process_1 = require("node:process");
8
8
  const gates_1 = require("../gates");
9
9
  const init_1 = require("../init");
10
+ const http_1 = require("../http");
10
11
  function resolveNetworkContext() {
11
12
  const cfg = (0, config_1.readConfig)();
12
13
  const projectId = process.env.AGENTBRIDGE_PROJECT_ID ?? cfg.projectId ?? "";
@@ -78,6 +79,60 @@ function renderRecoveryStatusBlock(input) {
78
79
  lines.push("");
79
80
  return lines.join("\n");
80
81
  }
82
+ function normalizeBootstrapError(error) {
83
+ if ((0, http_1.isCliHttpError)(error) && error.status === 422) {
84
+ const parsed = (0, http_1.parseCliHttpErrorBody)(error);
85
+ const details = [];
86
+ const nestedError = parsed && typeof parsed.error === "object" && parsed.error !== null
87
+ ? parsed.error
88
+ : null;
89
+ const fieldErrors = nestedError && typeof nestedError.fieldErrors === "object" && nestedError.fieldErrors !== null
90
+ ? nestedError.fieldErrors
91
+ : null;
92
+ if (fieldErrors) {
93
+ for (const [field, value] of Object.entries(fieldErrors)) {
94
+ if (Array.isArray(value) && value.length > 0) {
95
+ details.push(`${field}: ${String(value[0])}`);
96
+ }
97
+ }
98
+ }
99
+ const detailText = details.length > 0 ? ` (${details.join("; ")})` : "";
100
+ return (0, errors_1.catalogCliError)("CONFIG_INCOMPLETE", {
101
+ what: `Recovery bootstrap payload was rejected by server validation${detailText}.`,
102
+ why: "The server cannot activate recovery until required bootstrap fields pass validation.",
103
+ next: "Retry `agentbridge recover`. If it fails again, run with AGENTBRIDGE_DEBUG=1 and share output.",
104
+ });
105
+ }
106
+ if (error instanceof Error) {
107
+ const message = error.message;
108
+ if (/does not have any commits yet/i.test(message) ||
109
+ /bad revision ['"]head['"]/i.test(message) ||
110
+ /not a git repository/i.test(message)) {
111
+ return new errors_1.SafeCliError({
112
+ code: "CONFIG_INCOMPLETE",
113
+ category: "CONFIG_ERROR",
114
+ what: "Repository has no usable git history for recovery mapping.",
115
+ why: "Recover infers baseline context from commit history and cannot proceed without it.",
116
+ next: [
117
+ "Create an initial commit, then rerun recover:",
118
+ "1) git add .",
119
+ "2) git commit -m \"Initial commit\"",
120
+ "3) agentbridge recover",
121
+ ].join("\n"),
122
+ });
123
+ }
124
+ if (/payload too large after compaction/i.test(message)) {
125
+ return new errors_1.SafeCliError({
126
+ code: "CONFIG_INCOMPLETE",
127
+ category: "CONFIG_ERROR",
128
+ what: "Repository evidence exceeded bootstrap payload limits.",
129
+ why: "The recovery evidence payload could not be compacted enough for server ingestion.",
130
+ next: "Run `agentbridge init` for an interactive baseline, or retry recover after reducing repository noise.",
131
+ });
132
+ }
133
+ }
134
+ return error;
135
+ }
81
136
  async function runRecover(options = {}) {
82
137
  process.exitCode = 0;
83
138
  process.stdout.write([
@@ -127,8 +182,15 @@ async function runRecover(options = {}) {
127
182
  ? "build recovery baseline from repository evidence"
128
183
  : "force-refresh recovery baseline from repository evidence";
129
184
  process.stdout.write("Building recovery baseline now...\n");
130
- await (0, init_1.runBootstrapRecovery)(ctx);
131
- packet = await (0, server_sync_1.fetchProjectPacket)(ctx);
185
+ try {
186
+ await (0, init_1.runBootstrapRecovery)(ctx);
187
+ packet = await (0, server_sync_1.fetchProjectPacket)(ctx);
188
+ }
189
+ catch (error) {
190
+ process.stderr.write(`${(0, errors_1.renderCliError)(normalizeBootstrapError(error))}\n`);
191
+ process.exitCode = 1;
192
+ return;
193
+ }
132
194
  }
133
195
  const afterPacket = packet;
134
196
  if (baselineRequired(afterPacket)) {
@@ -395,6 +395,29 @@ function renderOtherSessions(lines, sessionIds) {
395
395
  ...sessionIds.map((id) => `- ${id}`),
396
396
  ];
397
397
  }
398
+ async function resolveExecutionSurfaceIdForStart(ctx, opts, cfgExecutionSurfaceId) {
399
+ const explicitExecutionSurfaceId = opts.executionSurfaceId?.trim();
400
+ if (explicitExecutionSurfaceId) {
401
+ (0, config_1.updateConfig)({ executionSurfaceId: explicitExecutionSurfaceId });
402
+ return explicitExecutionSurfaceId;
403
+ }
404
+ const configuredExecutionSurfaceId = cfgExecutionSurfaceId?.trim();
405
+ if (configuredExecutionSurfaceId) {
406
+ return configuredExecutionSurfaceId;
407
+ }
408
+ try {
409
+ const callerPacket = await (0, server_sync_1.fetchCallerIdentityPacket)(ctx);
410
+ const discoveredExecutionSurfaceId = callerPacket?.execution_surface?.id?.trim() || null;
411
+ if (discoveredExecutionSurfaceId) {
412
+ (0, config_1.updateConfig)({ executionSurfaceId: discoveredExecutionSurfaceId });
413
+ return discoveredExecutionSurfaceId;
414
+ }
415
+ }
416
+ catch {
417
+ // Keep the existing catalog error path if caller identity discovery fails.
418
+ }
419
+ return null;
420
+ }
398
421
  async function executeStartWorkSession(opts) {
399
422
  const summary = trimRequired(opts.summary ?? "", "--summary");
400
423
  const scope = trimRequired(opts.scope ?? "", "--scope");
@@ -406,7 +429,7 @@ async function executeStartWorkSession(opts) {
406
429
  if (!explicitAgentId && !configuredActiveAgentId) {
407
430
  throw (0, errors_1.catalogCliError)("START_MISSING_ACTIVE_AGENT");
408
431
  }
409
- const executionSurfaceId = cfg.executionSurfaceId?.trim();
432
+ const executionSurfaceId = await resolveExecutionSurfaceIdForStart(ctx, opts, cfg.executionSurfaceId);
410
433
  if (!executionSurfaceId) {
411
434
  throw (0, errors_1.catalogCliError)("START_EXECUTION_SURFACE_REQUIRED");
412
435
  }
@@ -547,15 +570,16 @@ async function executeStartWorkSession(opts) {
547
570
  async function runSmartStart(opts = {}) {
548
571
  const cfg = (0, config_1.readConfig)();
549
572
  await ensureProjectAccess();
573
+ const ctx = (0, config_1.contextFromConfig)();
550
574
  const explicitAgentId = opts.agentId?.trim();
551
575
  const configuredActiveAgentId = cfg.activeAgentId?.trim();
552
576
  if (!explicitAgentId && !configuredActiveAgentId) {
553
577
  throw (0, errors_1.catalogCliError)("START_MISSING_ACTIVE_AGENT");
554
578
  }
555
- if (!cfg.executionSurfaceId?.trim()) {
579
+ const executionSurfaceId = await resolveExecutionSurfaceIdForStart(ctx, opts, cfg.executionSurfaceId);
580
+ if (!executionSurfaceId) {
556
581
  throw (0, errors_1.catalogCliError)("START_EXECUTION_SURFACE_REQUIRED");
557
582
  }
558
- const ctx = (0, config_1.contextFromConfig)();
559
583
  const resolution = await (0, work_context_resolver_1.resolveWorkContext)(ctx, {
560
584
  allowServerCurrentForCr: true,
561
585
  includeOtherActiveSessions: true,
@@ -186,7 +186,13 @@ const CATALOG = {
186
186
  title: "Execution surface missing.",
187
187
  what: "No executionSurfaceId is saved in .agentbridge/config.json.",
188
188
  why: "The server needs a stable execution surface to open a tracked session.",
189
- next: "Run `agentbridge watch --execution-surface <surface-id>` once to persist it.",
189
+ next: [
190
+ "Run:",
191
+ " agentbridge connect --project <project-id> --api-key <key> --api-base-url <url>",
192
+ "Then:",
193
+ " agentbridge doctor",
194
+ "If this continues, AgentBridge could not create an AgentConnection/execution surface for this project.",
195
+ ].join("\n"),
190
196
  },
191
197
  START_AGENT_INVALID: {
192
198
  code: "START_AGENT_INVALID",
package/dist/index.js CHANGED
@@ -49,7 +49,7 @@ const COMMAND_HELP = {
49
49
  use: " agentbridge use <agent-id>\n" +
50
50
  " Set the active agent identity used by `watch` and `start`.\n",
51
51
  start: " agentbridge start [\"intent\"] [\"scope\"]\n" +
52
- " Advanced: agentbridge start --summary \"<summary>\" --scope \"<path-or-glob>\" [--domain <name>] [--agent <id>] [--change-request <cr_id>] [--resume]\n" +
52
+ " Advanced: agentbridge start --summary \"<summary>\" --scope \"<path-or-glob>\" [--domain <name>] [--agent <id>] [--execution-surface <es_id>] [--change-request <cr_id>] [--resume]\n" +
53
53
  " Start supervised work. Without flags, proposes a smart work contract and asks for confirmation.\n" +
54
54
  " --resume Continue the active change request/session instead of creating a new one.\n",
55
55
  watch: " agentbridge watch [--change-request <cr_id>] [--execution-surface <es_id>] [--allow-dirty] [--daemon] [--once]\n" +
@@ -350,6 +350,7 @@ async function main() {
350
350
  scopeFlagProvided,
351
351
  domain: flags["--domain"],
352
352
  agentId: flags["--agent"],
353
+ executionSurfaceId: flags["--execution-surface"],
353
354
  changeRequestId: flags["--change-request"],
354
355
  resume: flags["--resume"] === "true",
355
356
  });
package/dist/init.js CHANGED
@@ -104,16 +104,24 @@ function assertInsideGitRepo() {
104
104
  function normalizeToken(value) {
105
105
  return value.trim().toLowerCase().replace(/[_\s]+/g, "-");
106
106
  }
107
- function classifyDomainKey(path) {
108
- const normalized = path.replace(/\\/g, "/");
109
- const srcMatch = normalized.match(/^src\/([^/]+)\//);
110
- if (srcMatch) {
111
- const folder = normalizeToken(srcMatch[1] ?? "");
107
+ function normalizePath(path) {
108
+ return path.replace(/\\/g, "/").replace(/^\.\//, "");
109
+ }
110
+ function deriveDomainKeyFromPath(path) {
111
+ const normalized = normalizePath(path);
112
+ const packageSourcesMatch = normalized.match(/^([^/]+)\/Sources\/([^/]+)\//);
113
+ if (packageSourcesMatch) {
114
+ const folder = normalizeToken(packageSourcesMatch[2] ?? "");
112
115
  if (folder === "supabase")
113
116
  return "database";
114
- if (folder)
115
- return folder;
116
- return null;
117
+ return folder || null;
118
+ }
119
+ const srcLikeMatch = normalized.match(/^(src|Sources|modules|Modules|feature|features)\/([^/]+)\//);
120
+ if (srcLikeMatch) {
121
+ const folder = normalizeToken(srcLikeMatch[2] ?? "");
122
+ if (folder === "supabase")
123
+ return "database";
124
+ return folder || null;
117
125
  }
118
126
  if (normalized.startsWith("db/") || normalized.startsWith("prisma/") || normalized.startsWith("supabase/")) {
119
127
  return "database";
@@ -123,6 +131,29 @@ function classifyDomainKey(path) {
123
131
  }
124
132
  return null;
125
133
  }
134
+ function extractDomainPrefix(path) {
135
+ const normalized = normalizePath(path);
136
+ const packageSourcesMatch = normalized.match(/^([^/]+)\/Sources\/([^/]+)\//);
137
+ if (packageSourcesMatch) {
138
+ return `${packageSourcesMatch[1]}/Sources/${packageSourcesMatch[2]}/`;
139
+ }
140
+ const srcLikeMatch = normalized.match(/^(src|Sources|modules|Modules|feature|features)\/([^/]+)\//);
141
+ if (srcLikeMatch) {
142
+ return `${srcLikeMatch[1]}/${srcLikeMatch[2]}/`;
143
+ }
144
+ if (normalized.startsWith("db/"))
145
+ return "db/";
146
+ if (normalized.startsWith("prisma/"))
147
+ return "prisma/";
148
+ if (normalized.startsWith("supabase/"))
149
+ return "supabase/";
150
+ if (/^supabase_.*\.sql$/i.test(normalized))
151
+ return "supabase_*.sql";
152
+ return null;
153
+ }
154
+ function classifyDomainKey(path) {
155
+ return deriveDomainKeyFromPath(path);
156
+ }
126
157
  function tierForDomainKey(domainKey, files) {
127
158
  if (domainKey === "sessions") {
128
159
  const highRisk = files.some((file) => /(token|auth|state|orchestrator|repository|session)/i.test(file));
@@ -148,9 +179,11 @@ function confidenceForDomain(fileCount) {
148
179
  }
149
180
  function collectWorkspaceDomainSeeds() {
150
181
  const seeds = new Set();
151
- const srcPath = (0, node_path_1.resolve)(process.cwd(), "src");
152
- if ((0, node_fs_1.existsSync)(srcPath)) {
153
- for (const entry of (0, node_fs_1.readdirSync)(srcPath, { withFileTypes: true })) {
182
+ for (const root of ["src", "Sources", "modules", "Modules", "feature", "features"]) {
183
+ const rootPath = (0, node_path_1.resolve)(process.cwd(), root);
184
+ if (!(0, node_fs_1.existsSync)(rootPath))
185
+ continue;
186
+ for (const entry of (0, node_fs_1.readdirSync)(rootPath, { withFileTypes: true })) {
154
187
  if (!entry.isDirectory())
155
188
  continue;
156
189
  const folder = normalizeToken(entry.name);
@@ -162,6 +195,24 @@ function collectWorkspaceDomainSeeds() {
162
195
  }
163
196
  }
164
197
  }
198
+ for (const topLevelEntry of (0, node_fs_1.readdirSync)(process.cwd(), { withFileTypes: true })) {
199
+ if (!topLevelEntry.isDirectory())
200
+ continue;
201
+ const packageSourcesPath = (0, node_path_1.resolve)(process.cwd(), topLevelEntry.name, "Sources");
202
+ if (!(0, node_fs_1.existsSync)(packageSourcesPath))
203
+ continue;
204
+ for (const sourceEntry of (0, node_fs_1.readdirSync)(packageSourcesPath, { withFileTypes: true })) {
205
+ if (!sourceEntry.isDirectory())
206
+ continue;
207
+ const folder = normalizeToken(sourceEntry.name);
208
+ if (folder === "supabase") {
209
+ seeds.add("database");
210
+ }
211
+ else if (folder) {
212
+ seeds.add(folder);
213
+ }
214
+ }
215
+ }
165
216
  for (const dbFolder of ["db", "prisma", "supabase"]) {
166
217
  if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(process.cwd(), dbFolder))) {
167
218
  seeds.add("database");
@@ -171,10 +222,13 @@ function collectWorkspaceDomainSeeds() {
171
222
  }
172
223
  function refineRecoveredDomains(inferredClusters, evidence, architectureDomains) {
173
224
  const domainFiles = new Map();
225
+ const domainPrefixes = new Map();
174
226
  const workspaceSeeds = collectWorkspaceDomainSeeds();
175
227
  for (const seed of workspaceSeeds) {
176
228
  if (!domainFiles.has(seed))
177
229
  domainFiles.set(seed, new Set());
230
+ if (!domainPrefixes.has(seed))
231
+ domainPrefixes.set(seed, new Set());
178
232
  }
179
233
  for (const file of evidence.files) {
180
234
  const key = classifyDomainKey(file);
@@ -183,6 +237,12 @@ function refineRecoveredDomains(inferredClusters, evidence, architectureDomains)
183
237
  if (!domainFiles.has(key))
184
238
  domainFiles.set(key, new Set());
185
239
  domainFiles.get(key)?.add(file);
240
+ const prefix = extractDomainPrefix(file);
241
+ if (prefix) {
242
+ if (!domainPrefixes.has(key))
243
+ domainPrefixes.set(key, new Set());
244
+ domainPrefixes.get(key)?.add(prefix);
245
+ }
186
246
  }
187
247
  for (const cluster of inferredClusters) {
188
248
  const clusterPaths = [...cluster.files, ...cluster.path_prefixes];
@@ -207,6 +267,12 @@ function refineRecoveredDomains(inferredClusters, evidence, architectureDomains)
207
267
  for (const file of cluster.files) {
208
268
  if (classifyDomainKey(file) === chosenKey) {
209
269
  domainFiles.get(chosenKey)?.add(file);
270
+ const prefix = extractDomainPrefix(file);
271
+ if (prefix) {
272
+ if (!domainPrefixes.has(chosenKey))
273
+ domainPrefixes.set(chosenKey, new Set());
274
+ domainPrefixes.get(chosenKey)?.add(prefix);
275
+ }
210
276
  }
211
277
  }
212
278
  }
@@ -237,12 +303,14 @@ function refineRecoveredDomains(inferredClusters, evidence, architectureDomains)
237
303
  return Array.from(domainFiles.entries())
238
304
  .map(([key, filesSet]) => {
239
305
  const files = Array.from(filesSet).sort((a, b) => a.localeCompare(b));
306
+ const inferredPrefixes = Array.from(domainPrefixes.get(key) ?? []);
240
307
  const pathPatterns = DOMAIN_PREFIX_BY_KEY[key] ??
308
+ (inferredPrefixes.length > 0 ? inferredPrefixes : undefined) ??
241
309
  (key === "database"
242
310
  ? ["db/", "prisma/", "supabase/", "supabase_*.sql"]
243
311
  : key === "shared"
244
312
  ? ["src/shared/"]
245
- : [`src/${key}/`]);
313
+ : [`${key}/`]);
246
314
  const primaryPrefix = pathPatterns[0] ? pathPatterns[0].replace(/\/$/, "") : key;
247
315
  const protectionTier = tierForDomainKey(key, files);
248
316
  return {
@@ -132,6 +132,17 @@ async function fetchCallerIdentityPacket(ctx) {
132
132
  typeof domainCandidate.name === "string"
133
133
  ? { id: domainCandidate.id, name: domainCandidate.name }
134
134
  : null;
135
+ const executionSurfaceCandidate = typeof response.execution_surface === "object" && response.execution_surface !== null
136
+ ? response.execution_surface
137
+ : null;
138
+ const executionSurface = executionSurfaceCandidate && typeof executionSurfaceCandidate.id === "string"
139
+ ? {
140
+ id: executionSurfaceCandidate.id,
141
+ ...(typeof executionSurfaceCandidate.display_name === "string"
142
+ ? { display_name: executionSurfaceCandidate.display_name }
143
+ : {}),
144
+ }
145
+ : null;
135
146
  return {
136
147
  identity_model: "work_identity",
137
148
  work_identity: {
@@ -139,6 +150,7 @@ async function fetchCallerIdentityPacket(ctx) {
139
150
  name: workIdentity.name,
140
151
  domain,
141
152
  },
153
+ execution_surface: executionSurface,
142
154
  };
143
155
  }
144
156
  async function postObservedDiff(ctx, binding, payload) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentbridge1/cli",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "CLI for AgentBridge — structured AI agent work sessions with domain authority and approval gates",
5
5
  "type": "commonjs",
6
6
  "bin": {