@agentbridge1/cli 0.0.4 → 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-02T04:55:04.008Z",
3
- "gitHead": "86fed7f",
4
- "sourceLatestMtime": "2026-06-02T04:51:46.614Z",
5
- "sourceLatestFile": "src/commands/connect.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
  }
@@ -84,6 +84,113 @@ async function listAgents(projectId, apiKey, apiBaseUrl) {
84
84
  const body = (await res.json());
85
85
  return Array.isArray(body.agents) ? body.agents : [];
86
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
+ }
87
194
  async function runConnect(options = {}) {
88
195
  process.stdout.write("AgentBridge connect\n");
89
196
  process.stdout.write("─────────────────────────────────────────\n");
@@ -142,13 +249,86 @@ async function runConnect(options = {}) {
142
249
  throw err;
143
250
  }
144
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
+ }
145
314
  // Persist credentials
146
- (0, config_1.updateConfig)({ projectId, apiKey, apiBaseUrl });
315
+ (0, config_1.updateConfig)({
316
+ projectId,
317
+ apiKey: effectiveApiKey,
318
+ apiBaseUrl,
319
+ ...(executionSurfaceId ? { executionSurfaceId } : {}),
320
+ });
147
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
+ }
148
328
  // Pick or auto-select an agent identity
149
329
  let activeAgentId = options.agentId ?? cfg.activeAgentId;
150
330
  if (!activeAgentId) {
151
- const agents = await listAgents(projectId, apiKey, apiBaseUrl);
331
+ const agents = await listAgents(projectId, effectiveApiKey, apiBaseUrl);
152
332
  if (agents.length === 1 && agents[0]) {
153
333
  activeAgentId = agents[0].id;
154
334
  (0, config_1.updateConfig)({ activeAgentId });
@@ -159,7 +339,7 @@ async function runConnect(options = {}) {
159
339
  for (const a of agents) {
160
340
  process.stdout.write(` ${a.id} ${a.name ?? ""} ${a.role ?? ""}\n`.trimEnd() + "\n");
161
341
  }
162
- 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');
163
343
  }
164
344
  }
165
345
  else {
@@ -169,24 +349,23 @@ async function runConnect(options = {}) {
169
349
  process.stdout.write("\n");
170
350
  process.stdout.write(`Project: ${projectName}\n`);
171
351
  process.stdout.write(`Agents: ${agentCount}\n`);
352
+ process.stdout.write(`Execution surface: ${executionSurfaceId}\n`);
172
353
  process.stdout.write(`Server: ${apiBaseUrl}\n`);
173
354
  process.stdout.write("\n");
174
355
  if (activeAgentId) {
175
- process.stdout.write("✓ Connected. Next steps:\n");
356
+ process.stdout.write("✓ Connected and ready.\n");
176
357
  process.stdout.write([
177
358
  "",
178
- " 1. Start a work session:",
179
- " agentbridge start --summary \"your task\" --scope \"src/\"",
180
- "",
181
- " 2. Run the watcher (keep it running in a terminal):",
182
- " agentbridge watch",
183
- "",
184
- " 3. Code normally — AgentBridge watches in the background.",
185
- " Approvals surface when you cross domain boundaries.",
359
+ "Next:",
360
+ " agentbridge doctor",
361
+ " agentbridge start",
186
362
  "",
187
363
  ].join("\n"));
188
364
  }
189
365
  else {
190
- 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");
191
370
  }
192
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.4",
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": {