@agentbridge1/cli 0.0.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 (69) hide show
  1. package/bin/agentbridge.js +11 -0
  2. package/dist/acceptance-block.js +21 -0
  3. package/dist/acceptance-preflight.js +91 -0
  4. package/dist/api-client.js +6 -0
  5. package/dist/authority-request.js +25 -0
  6. package/dist/briefing.js +26 -0
  7. package/dist/bug-registry.js +350 -0
  8. package/dist/build-info.json +6 -0
  9. package/dist/canonical-state.js +11 -0
  10. package/dist/claimed-paths.js +42 -0
  11. package/dist/cli-failure-log.js +34 -0
  12. package/dist/commands/accept.js +241 -0
  13. package/dist/commands/attention.js +85 -0
  14. package/dist/commands/autopilot.js +93 -0
  15. package/dist/commands/bug.js +106 -0
  16. package/dist/commands/check.js +283 -0
  17. package/dist/commands/connect.js +159 -0
  18. package/dist/commands/dist-freshness.js +105 -0
  19. package/dist/commands/doctor.js +300 -0
  20. package/dist/commands/done.js +292 -0
  21. package/dist/commands/handoff.js +189 -0
  22. package/dist/commands/handshake.js +78 -0
  23. package/dist/commands/health.js +154 -0
  24. package/dist/commands/identity.js +57 -0
  25. package/dist/commands/init.js +5 -0
  26. package/dist/commands/memory.js +400 -0
  27. package/dist/commands/next.js +21 -0
  28. package/dist/commands/precommit-check.js +17 -0
  29. package/dist/commands/recover.js +116 -0
  30. package/dist/commands/session.js +229 -0
  31. package/dist/commands/setup-mcp.js +56 -0
  32. package/dist/commands/start.js +626 -0
  33. package/dist/commands/status.js +486 -0
  34. package/dist/commands/use.js +13 -0
  35. package/dist/commands/verify.js +264 -0
  36. package/dist/commands/version.js +32 -0
  37. package/dist/commands/watch.js +1718 -0
  38. package/dist/config.js +55 -0
  39. package/dist/domain-resolution.js +63 -0
  40. package/dist/error-catalog.js +494 -0
  41. package/dist/errors.js +276 -0
  42. package/dist/file-fingerprints.js +45 -0
  43. package/dist/gates.js +200 -0
  44. package/dist/git-evidence.js +285 -0
  45. package/dist/git-status.js +81 -0
  46. package/dist/http.js +151 -0
  47. package/dist/index.js +622 -0
  48. package/dist/init.js +458 -0
  49. package/dist/memory-context-render.js +51 -0
  50. package/dist/operator-snapshot.js +99 -0
  51. package/dist/precommit.js +72 -0
  52. package/dist/preflight-changed-files.js +109 -0
  53. package/dist/proof-guidance.js +110 -0
  54. package/dist/redact-secrets.js +15 -0
  55. package/dist/revert-crossing.js +73 -0
  56. package/dist/server-sync.js +433 -0
  57. package/dist/session-state.js +138 -0
  58. package/dist/session.js +89 -0
  59. package/dist/supervision.js +212 -0
  60. package/dist/terminal-ui.js +18 -0
  61. package/dist/test-runner.js +62 -0
  62. package/dist/types.js +2 -0
  63. package/dist/verification-conditions.js +185 -0
  64. package/dist/watch-core.js +208 -0
  65. package/dist/watch-packet-handshake.js +71 -0
  66. package/dist/watcher.js +62 -0
  67. package/dist/work-context-resolver.js +412 -0
  68. package/dist/work-contract.js +110 -0
  69. package/package.json +44 -0
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderHandshakeRefusalBlock = renderHandshakeRefusalBlock;
4
+ exports.applyHandshakeRefusal = applyHandshakeRefusal;
5
+ exports.renderProjectPacketBanner = renderProjectPacketBanner;
6
+ exports.renderProjectPacketOneLiner = renderProjectPacketOneLiner;
7
+ exports.renderDomainPacketBanner = renderDomainPacketBanner;
8
+ function renderHandshakeRefusalBlock(file, crossingDomain) {
9
+ const domainLabel = crossingDomain ?? "unknown";
10
+ return [
11
+ `✋ Edit refused: "${file}" is in domain "${domainLabel}" which requires a cross-domain`,
12
+ " work permit (AgentHandshake) before you can claim it.",
13
+ "",
14
+ " Create the permit, then re-edit:",
15
+ " agentbridge handshake create \\",
16
+ ` --domain "${domainLabel}" \\`,
17
+ ' --action "<≥20 char description of what you intend to change>" \\',
18
+ ' --reason "<≥20 char rationale>"',
19
+ "",
20
+ " Watch will surface the file change again on your next save once the handshake",
21
+ " is requested. Acceptance will remain blocked until the handshake is resolved",
22
+ " (status: resolved by target, or acknowledged with explicit close approval).",
23
+ ].join("\n");
24
+ }
25
+ function applyHandshakeRefusal(state, file, outcome, revertFile) {
26
+ const crossing = state.crossings.find((c) => c.file === file);
27
+ if (crossing) {
28
+ crossing.status = "pending_handshake";
29
+ if (outcome.crossingDomain) {
30
+ crossing.domain = outcome.crossingDomain;
31
+ }
32
+ }
33
+ state.changedFiles = state.changedFiles.filter((changed) => changed !== file);
34
+ revertFile(file);
35
+ }
36
+ function renderProjectPacketBanner(packet, cfgDomains = []) {
37
+ const tierByDomain = new Map(cfgDomains.map((d) => [d.domain, d.tier]));
38
+ const ownerByDomain = new Map(cfgDomains.map((d) => [d.domain, d.ownerAgentId]));
39
+ const mode = packet.project_mode ?? "unknown";
40
+ const charter = packet.charter_summary?.purpose?.slice(0, 80) ?? "none";
41
+ const lines = [
42
+ `Project packet (recovery_mode: ${mode}, charter: ${charter}):`,
43
+ ` Domains (${packet.domains_summary.length}):`,
44
+ ];
45
+ for (const domain of packet.domains_summary.slice(0, 8)) {
46
+ const tier = tierByDomain.get(domain.domain_name) ?? "unknown";
47
+ const owner = ownerByDomain.get(domain.domain_name) ?? domain.label ?? "unknown";
48
+ const freshness = domain.freshness?.staleness ?? "unknown";
49
+ lines.push(` - ${domain.domain_name}: tier=${tier}, owner=${owner}, freshness=${freshness}`);
50
+ }
51
+ const invariants = packet.global_rules?.length ?? 0;
52
+ const risks = packet.global_open_risks?.length ?? 0;
53
+ lines.push(` Known invariants: ${invariants}; Open risks: ${risks}`);
54
+ return lines.join("\n");
55
+ }
56
+ function renderProjectPacketOneLiner(packet) {
57
+ return `Project packet: ${packet.domains_summary.length} domains, mode=${packet.project_mode ?? "unknown"}`;
58
+ }
59
+ function renderDomainPacketBanner(entry) {
60
+ const lines = [
61
+ `Domain packet (${entry.domain_name}):`,
62
+ ` Known traps: ${entry.known_traps.length}; Owned paths: ${entry.owned_paths.length}`,
63
+ ];
64
+ for (const trap of entry.known_traps.slice(0, 3)) {
65
+ lines.push(` - trap: ${trap}`);
66
+ }
67
+ for (const risk of entry.open_risks.slice(0, 3)) {
68
+ lines.push(` - risk: ${risk}`);
69
+ }
70
+ return lines.join("\n");
71
+ }
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.shouldIgnorePath = shouldIgnorePath;
7
+ exports.startWatcher = startWatcher;
8
+ const chokidar_1 = __importDefault(require("chokidar"));
9
+ const IGNORED_SEGMENTS = [
10
+ "/.git/",
11
+ "/node_modules/",
12
+ "/.agentbridge/",
13
+ "/.agents/",
14
+ "/.cursor/",
15
+ "/dist/",
16
+ "/build/",
17
+ "/.next/",
18
+ "/coverage/",
19
+ "/.turbo/",
20
+ "/.cache/",
21
+ "/DerivedData/",
22
+ "/logs/",
23
+ "/tmp/",
24
+ "/temp/",
25
+ "/.tmp/",
26
+ ];
27
+ function shouldIgnorePath(targetPath) {
28
+ const normalized = targetPath.replace(/\\/g, "/");
29
+ if (normalized.startsWith(".") && !normalized.startsWith("./src/")) {
30
+ return true;
31
+ }
32
+ return IGNORED_SEGMENTS.some((segment) => normalized.includes(segment));
33
+ }
34
+ function startWatcher(root, onChange) {
35
+ const watcher = chokidar_1.default.watch(root, {
36
+ ignoreInitial: true,
37
+ ignored: [
38
+ "**/.git/**",
39
+ "**/node_modules/**",
40
+ "**/.agentbridge/**",
41
+ "**/.agents/**",
42
+ "**/.cursor/**",
43
+ "**/dist/**",
44
+ "**/build/**",
45
+ "**/.next/**",
46
+ "**/coverage/**",
47
+ "**/.turbo/**",
48
+ "**/.cache/**",
49
+ "**/DerivedData/**",
50
+ "**/logs/**",
51
+ "**/tmp/**",
52
+ "**/temp/**",
53
+ (targetPath) => shouldIgnorePath(String(targetPath)),
54
+ ],
55
+ });
56
+ watcher.on("add", (path) => onChange(path));
57
+ watcher.on("change", (path) => onChange(path));
58
+ watcher.on("unlink", (path) => onChange(path));
59
+ return () => {
60
+ void watcher.close();
61
+ };
62
+ }
@@ -0,0 +1,412 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NON_TERMINAL_SESSION_STATUSES = void 0;
4
+ exports.resolveEffectiveChangeRequestId = resolveEffectiveChangeRequestId;
5
+ exports.isNonTerminalSessionStatus = isNonTerminalSessionStatus;
6
+ exports.resolveWorkContext = resolveWorkContext;
7
+ exports.requireWorkContextBinding = requireWorkContextBinding;
8
+ exports.renderWorkContextGuidance = renderWorkContextGuidance;
9
+ exports.renderWorkContextLines = renderWorkContextLines;
10
+ exports.findSingleActiveSessionForChangeRequest = findSingleActiveSessionForChangeRequest;
11
+ const config_1 = require("./config");
12
+ const errors_1 = require("./errors");
13
+ const error_catalog_1 = require("./error-catalog");
14
+ const http_1 = require("./http");
15
+ const session_state_1 = require("./session-state");
16
+ const server_sync_1 = require("./server-sync");
17
+ /** Non-terminal work-session statuses (matches server acceptance-check resolver). */
18
+ exports.NON_TERMINAL_SESSION_STATUSES = new Set([
19
+ "active",
20
+ "blocked",
21
+ "pending_boundary_approval",
22
+ "blocked_closed",
23
+ ]);
24
+ function resolveEffectiveChangeRequestContext(override, useConfigActiveChangeRequestId = true) {
25
+ const fromOverride = override?.trim();
26
+ if (fromOverride)
27
+ return { changeRequestId: fromOverride, source: "explicit CR" };
28
+ const fromEnv = process.env.AGENTBRIDGE_CHANGE_REQUEST_ID?.trim();
29
+ if (fromEnv)
30
+ return { changeRequestId: fromEnv, source: "explicit CR" };
31
+ if (useConfigActiveChangeRequestId) {
32
+ const cfg = (0, config_1.readConfig)();
33
+ const fromConfig = cfg.activeChangeRequestId?.trim();
34
+ if (fromConfig)
35
+ return { changeRequestId: fromConfig, source: "activeChangeRequestId" };
36
+ }
37
+ return { changeRequestId: null, source: "none" };
38
+ }
39
+ function resolveEffectiveChangeRequestId(override, useConfigActiveChangeRequestId = true) {
40
+ return resolveEffectiveChangeRequestContext(override, useConfigActiveChangeRequestId)
41
+ .changeRequestId;
42
+ }
43
+ function isNonTerminalSessionStatus(status) {
44
+ return exports.NON_TERMINAL_SESSION_STATUSES.has(status);
45
+ }
46
+ function terminalMessage(status) {
47
+ if (status === "closed")
48
+ return "Work session is closed.";
49
+ if (status === "cancelled")
50
+ return "Work session was cancelled.";
51
+ return `Work session is not active (status: ${status}).`;
52
+ }
53
+ async function loadActiveSessions(ctx) {
54
+ const sessions = await (0, server_sync_1.listWorkSessions)(ctx, { status: "all" });
55
+ return sessions.filter((s) => isNonTerminalSessionStatus(s.status));
56
+ }
57
+ async function tryBindSession(ctx, sessionId, requestedCr, skipAcceptanceFetch = false) {
58
+ let report;
59
+ let sessionStatus;
60
+ let sessionCr;
61
+ try {
62
+ const session = await (0, server_sync_1.getWorkSession)(ctx, sessionId);
63
+ sessionStatus = session.status;
64
+ sessionCr = session.change_request_id?.trim() ?? null;
65
+ if (!skipAcceptanceFetch) {
66
+ report = await (0, server_sync_1.fetchAcceptanceCheck)(ctx, { workSessionId: sessionId });
67
+ sessionCr = report.change_request_id?.trim() ?? null;
68
+ }
69
+ }
70
+ catch (err) {
71
+ if (err instanceof http_1.CliHttpError && err.status === 404) {
72
+ return { ok: false, reason: "missing" };
73
+ }
74
+ throw err;
75
+ }
76
+ if (!isNonTerminalSessionStatus(sessionStatus)) {
77
+ return { ok: false, reason: "terminal", status: sessionStatus, report };
78
+ }
79
+ if (requestedCr && sessionCr && sessionCr !== requestedCr) {
80
+ return { ok: false, reason: "mismatch", report };
81
+ }
82
+ return {
83
+ ok: true,
84
+ binding: {
85
+ workSessionId: skipAcceptanceFetch ? sessionId : report.work_session_id,
86
+ changeRequestId: sessionCr,
87
+ ...(report ? { report } : {}),
88
+ },
89
+ };
90
+ }
91
+ function partitionByCr(active, requestedCr) {
92
+ if (!requestedCr) {
93
+ return { onRequestedCr: active, other: [] };
94
+ }
95
+ const onRequestedCr = active.filter((s) => (s.change_request_id?.trim() ?? null) === requestedCr);
96
+ const other = active.filter((s) => (s.change_request_id?.trim() ?? null) !== requestedCr);
97
+ return { onRequestedCr, other };
98
+ }
99
+ /**
100
+ * Strict work-context resolution. Never binds to an unrelated active session.
101
+ */
102
+ async function resolveWorkContext(ctx, options = {}) {
103
+ const useConfigCr = options.useConfigActiveChangeRequestId !== false;
104
+ const skipAcceptanceFetch = options.skipAcceptanceFetchInBind === true;
105
+ const resolvedCrContext = resolveEffectiveChangeRequestContext(options.changeRequestId, useConfigCr);
106
+ const requestedCr = resolvedCrContext.changeRequestId;
107
+ const preferredId = options.preferredWorkSessionId?.trim() ?? null;
108
+ const local = (0, session_state_1.readSessionState)();
109
+ const localId = local?.serverSessionId?.trim() ?? null;
110
+ let usedServerCurrentForCr = false;
111
+ const withDiagnostics = (resolution) => ({
112
+ ...resolution,
113
+ diagnostics: {
114
+ localSessionId: localId,
115
+ resolvedSessionId: resolution.resolvedServerSessionId,
116
+ resolvedChangeRequestId: resolution.resolvedChangeRequestId,
117
+ usedConfigActiveChangeRequestId: resolvedCrContext.source === "activeChangeRequestId",
118
+ usedServerCurrentForCr,
119
+ },
120
+ });
121
+ if (preferredId) {
122
+ const attempt = await tryBindSession(ctx, preferredId, requestedCr, skipAcceptanceFetch);
123
+ if (attempt.ok) {
124
+ return withDiagnostics({
125
+ state: "current_session_resolved",
126
+ contextSource: "explicit session",
127
+ requestedChangeRequestId: requestedCr,
128
+ binding: attempt.binding,
129
+ ambiguousSessions: [],
130
+ otherActiveSessions: [],
131
+ localServerSessionId: localId,
132
+ resolvedServerSessionId: attempt.binding.workSessionId,
133
+ resolvedChangeRequestId: attempt.binding.changeRequestId,
134
+ message: `Bound to work session ${preferredId}.`,
135
+ });
136
+ }
137
+ if (attempt.reason === "missing") {
138
+ return withDiagnostics({
139
+ state: "stale_orphan_session_detected",
140
+ contextSource: "explicit session",
141
+ requestedChangeRequestId: requestedCr,
142
+ binding: null,
143
+ ambiguousSessions: [],
144
+ otherActiveSessions: [],
145
+ localServerSessionId: preferredId,
146
+ resolvedServerSessionId: null,
147
+ resolvedChangeRequestId: null,
148
+ message: `Work session ${preferredId} was not found on the server.`,
149
+ });
150
+ }
151
+ return withDiagnostics({
152
+ state: attempt.reason === "mismatch" ? "mismatch" : "current_session_closed",
153
+ contextSource: "explicit session",
154
+ requestedChangeRequestId: requestedCr,
155
+ binding: null,
156
+ ambiguousSessions: [],
157
+ otherActiveSessions: [],
158
+ localServerSessionId: preferredId,
159
+ resolvedServerSessionId: attempt.report?.work_session_id ?? preferredId,
160
+ resolvedChangeRequestId: attempt.report?.change_request_id?.trim() ?? null,
161
+ message: attempt.reason === "mismatch"
162
+ ? `Work session ${preferredId} does not match the requested change request.`
163
+ : terminalMessage(attempt.status ?? "closed"),
164
+ });
165
+ }
166
+ const contextSource = localId
167
+ ? "local session"
168
+ : resolvedCrContext.source;
169
+ if (localId) {
170
+ const attempt = await tryBindSession(ctx, localId, requestedCr, skipAcceptanceFetch);
171
+ if (attempt.ok) {
172
+ let otherActiveSessions = [];
173
+ if (options.includeOtherActiveSessions) {
174
+ const active = await loadActiveSessions(ctx);
175
+ otherActiveSessions = active.filter((s) => s.id !== attempt.binding.workSessionId);
176
+ }
177
+ return withDiagnostics({
178
+ state: "current_session_resolved",
179
+ contextSource,
180
+ requestedChangeRequestId: requestedCr,
181
+ binding: attempt.binding,
182
+ ambiguousSessions: [],
183
+ otherActiveSessions,
184
+ localServerSessionId: localId,
185
+ resolvedServerSessionId: attempt.binding.workSessionId,
186
+ resolvedChangeRequestId: attempt.binding.changeRequestId,
187
+ message: "Bound to local tracked work session.",
188
+ });
189
+ }
190
+ if (attempt.reason === "mismatch") {
191
+ const active = await loadActiveSessions(ctx);
192
+ const { other } = partitionByCr(active, requestedCr);
193
+ return withDiagnostics({
194
+ state: "mismatch",
195
+ contextSource,
196
+ requestedChangeRequestId: requestedCr,
197
+ binding: null,
198
+ ambiguousSessions: [],
199
+ otherActiveSessions: other,
200
+ localServerSessionId: localId,
201
+ resolvedServerSessionId: attempt.report?.work_session_id ?? localId,
202
+ resolvedChangeRequestId: attempt.report?.change_request_id?.trim() ?? null,
203
+ message: requestedCr
204
+ ? `Local session ${localId} belongs to change request ${attempt.report?.change_request_id ?? "none"}, not ${requestedCr}.`
205
+ : `Local session ${localId} does not match the requested change request context.`,
206
+ });
207
+ }
208
+ if (attempt.reason === "terminal") {
209
+ return withDiagnostics({
210
+ state: "current_session_closed",
211
+ contextSource,
212
+ requestedChangeRequestId: requestedCr,
213
+ binding: null,
214
+ ambiguousSessions: [],
215
+ otherActiveSessions: [],
216
+ localServerSessionId: localId,
217
+ resolvedServerSessionId: localId,
218
+ resolvedChangeRequestId: attempt.report?.change_request_id?.trim() ?? null,
219
+ message: terminalMessage(attempt.status ?? "closed"),
220
+ });
221
+ }
222
+ return withDiagnostics({
223
+ state: "stale_orphan_session_detected",
224
+ contextSource,
225
+ requestedChangeRequestId: requestedCr,
226
+ binding: null,
227
+ ambiguousSessions: [],
228
+ otherActiveSessions: [],
229
+ localServerSessionId: localId,
230
+ resolvedServerSessionId: null,
231
+ resolvedChangeRequestId: null,
232
+ message: `Local session ${localId} was not found on the server. Clear local state with agentbridge session abandon or start a new watch session.`,
233
+ });
234
+ }
235
+ const active = await loadActiveSessions(ctx);
236
+ const { onRequestedCr, other } = partitionByCr(active, requestedCr);
237
+ if (requestedCr && onRequestedCr.length > 1) {
238
+ return withDiagnostics({
239
+ state: "ambiguous_active_sessions",
240
+ contextSource,
241
+ requestedChangeRequestId: requestedCr,
242
+ binding: null,
243
+ ambiguousSessions: onRequestedCr,
244
+ otherActiveSessions: other,
245
+ localServerSessionId: null,
246
+ resolvedServerSessionId: null,
247
+ resolvedChangeRequestId: null,
248
+ message: `Multiple active work sessions match change request ${requestedCr}. Use agentbridge session list and session abandon to clean up.`,
249
+ });
250
+ }
251
+ if (requestedCr && onRequestedCr.length === 1 && options.allowServerCurrentForCr) {
252
+ const attempt = await tryBindSession(ctx, onRequestedCr[0].id, requestedCr, skipAcceptanceFetch);
253
+ if (attempt.ok) {
254
+ usedServerCurrentForCr = true;
255
+ return withDiagnostics({
256
+ state: "current_session_resolved",
257
+ contextSource,
258
+ requestedChangeRequestId: requestedCr,
259
+ binding: attempt.binding,
260
+ ambiguousSessions: [],
261
+ otherActiveSessions: other,
262
+ localServerSessionId: null,
263
+ resolvedServerSessionId: attempt.binding.workSessionId,
264
+ resolvedChangeRequestId: attempt.binding.changeRequestId,
265
+ message: "Bound to the single active server session for this change request.",
266
+ });
267
+ }
268
+ }
269
+ if (other.length > 0 ||
270
+ (requestedCr && onRequestedCr.length === 1) ||
271
+ (!requestedCr && onRequestedCr.length > 0)) {
272
+ const parts = [];
273
+ if (requestedCr && onRequestedCr.length === 1) {
274
+ parts.push(`An active server session exists for ${requestedCr} but is not linked locally.`);
275
+ parts.push("Run agentbridge watch with --change-request to bind, or use session inspect.");
276
+ }
277
+ else if (!requestedCr && onRequestedCr.length > 0) {
278
+ parts.push("Other active work sessions exist on the server but none are linked locally.");
279
+ }
280
+ else if (other.length > 0) {
281
+ parts.push("Other active work sessions exist on the server but none are linked locally.");
282
+ }
283
+ return withDiagnostics({
284
+ state: other.length > 0 || (!requestedCr && onRequestedCr.length > 0)
285
+ ? "other_active_sessions_exist"
286
+ : "no_current_session",
287
+ contextSource,
288
+ requestedChangeRequestId: requestedCr,
289
+ binding: null,
290
+ ambiguousSessions: [],
291
+ otherActiveSessions: requestedCr ? [...onRequestedCr, ...other] : onRequestedCr,
292
+ localServerSessionId: null,
293
+ resolvedServerSessionId: null,
294
+ resolvedChangeRequestId: null,
295
+ message: parts.join(" "),
296
+ });
297
+ }
298
+ return withDiagnostics({
299
+ state: "no_current_session",
300
+ contextSource,
301
+ requestedChangeRequestId: requestedCr,
302
+ binding: null,
303
+ ambiguousSessions: [],
304
+ otherActiveSessions: [],
305
+ localServerSessionId: null,
306
+ resolvedServerSessionId: null,
307
+ resolvedChangeRequestId: null,
308
+ message: requestedCr
309
+ ? `No active work session for change request ${requestedCr}.`
310
+ : "No active tracked work session. Run agentbridge watch to start work.",
311
+ });
312
+ }
313
+ function requireWorkContextBinding(resolution, commandLabel) {
314
+ if (resolution.state === "current_session_resolved" && resolution.binding) {
315
+ return resolution.binding;
316
+ }
317
+ const code = (0, error_catalog_1.workContextStateToCode)(resolution.state);
318
+ const entry = (0, error_catalog_1.catalogEntryForCode)(code);
319
+ const guidance = renderWorkContextGuidance(resolution);
320
+ const nextFromGuidance = guidance.find((line) => line.startsWith("Next action:"));
321
+ throw new errors_1.SafeCliError({
322
+ code,
323
+ category: entry?.category ?? "WORK_CONTEXT_ERROR",
324
+ what: `Cannot run ${commandLabel}: ${resolution.message}`,
325
+ why: entry?.why ?? "This command requires a linked local/server work session.",
326
+ next: nextFromGuidance?.replace(/^Next action:\s*/, "") ?? entry?.next ?? "Run `agentbridge watch`.",
327
+ });
328
+ }
329
+ function renderWorkContextGuidance(resolution) {
330
+ const lines = [];
331
+ if (resolution.requestedChangeRequestId) {
332
+ lines.push(`Requested change request: ${resolution.requestedChangeRequestId}`);
333
+ }
334
+ if (resolution.localServerSessionId) {
335
+ lines.push(`Local session id: ${resolution.localServerSessionId}`);
336
+ }
337
+ switch (resolution.state) {
338
+ case "mismatch":
339
+ lines.push("Next action: fix AGENTBRIDGE_CHANGE_REQUEST_ID / config activeChangeRequestId, or clear local session.");
340
+ lines.push(" agentbridge session abandon --reason \"wrong cr\"");
341
+ break;
342
+ case "ambiguous_active_sessions":
343
+ lines.push("Ambiguous active sessions for this change request:");
344
+ for (const s of resolution.ambiguousSessions) {
345
+ lines.push(` - ${s.id} (${s.status})`);
346
+ }
347
+ lines.push("Next action: agentbridge session abandon --session <id> --reason \"duplicate\"");
348
+ break;
349
+ case "other_active_sessions_exist":
350
+ case "no_current_session":
351
+ if (resolution.otherActiveSessions.length > 0) {
352
+ lines.push("Other active server sessions:");
353
+ for (const s of resolution.otherActiveSessions) {
354
+ const cr = s.change_request_id?.trim() ?? "no CR";
355
+ lines.push(` - ${s.id} CR=${cr} status=${s.status}`);
356
+ }
357
+ }
358
+ lines.push("Next action: agentbridge watch --change-request <cr> (does not auto-adopt unrelated sessions)");
359
+ break;
360
+ case "stale_orphan_session_detected":
361
+ lines.push("Next action: agentbridge session abandon --reason \"stale local id\"");
362
+ break;
363
+ case "current_session_closed":
364
+ lines.push("Next action: agentbridge session abandon --reason \"session closed\"");
365
+ break;
366
+ default:
367
+ break;
368
+ }
369
+ return lines;
370
+ }
371
+ function renderWorkContextLines(resolution) {
372
+ const code = (0, error_catalog_1.workContextStateToCode)(resolution.state);
373
+ const entry = (0, error_catalog_1.catalogEntryForCode)(code);
374
+ const header = [];
375
+ if (entry) {
376
+ header.push(`✗ Error code: ${code}`);
377
+ header.push(` What happened: ${entry.what}`);
378
+ header.push(` Why it matters: ${entry.why}`);
379
+ header.push("");
380
+ }
381
+ if (resolution.state === "mismatch") {
382
+ return [
383
+ ...header,
384
+ "Intended:",
385
+ `CR: ${resolution.requestedChangeRequestId ?? "none"}`,
386
+ `Session: ${resolution.localServerSessionId ?? "none"}`,
387
+ "",
388
+ "Resolved active server session:",
389
+ `CR: ${resolution.resolvedChangeRequestId ?? "none"}`,
390
+ `Session: ${resolution.resolvedServerSessionId ?? "none"}`,
391
+ "",
392
+ "This command will not continue because it may attach proof or acceptance to the wrong work.",
393
+ ...renderWorkContextGuidance(resolution),
394
+ ];
395
+ }
396
+ return [
397
+ ...header,
398
+ resolution.message,
399
+ ...renderWorkContextGuidance(resolution),
400
+ ];
401
+ }
402
+ /** Watch/open: find exactly one active session for a CR (explicit resume). */
403
+ async function findSingleActiveSessionForChangeRequest(ctx, changeRequestId) {
404
+ const cr = changeRequestId.trim();
405
+ const active = await loadActiveSessions(ctx);
406
+ const matches = active.filter((s) => (s.change_request_id?.trim() ?? null) === cr);
407
+ if (matches.length === 0)
408
+ return null;
409
+ if (matches.length > 1)
410
+ return { ambiguous: matches };
411
+ return { workSessionId: matches[0].id };
412
+ }
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.inferWorkContract = inferWorkContract;
4
+ exports.confirmWorkContract = confirmWorkContract;
5
+ const promises_1 = require("node:readline/promises");
6
+ const node_process_1 = require("node:process");
7
+ const config_1 = require("./config");
8
+ const domain_resolution_1 = require("./domain-resolution");
9
+ function humanizeBranchName(branchName) {
10
+ const cleaned = branchName
11
+ .trim()
12
+ .replace(/^refs\/heads\//, "")
13
+ .replace(/^feature\//, "")
14
+ .replace(/^fix\//, "")
15
+ .replace(/^chore\//, "")
16
+ .replace(/^bugfix\//, "")
17
+ .replace(/^hotfix\//, "")
18
+ .replace(/[._-]+/g, " ")
19
+ .trim();
20
+ if (!cleaned)
21
+ return "Continue current work";
22
+ return cleaned[0].toUpperCase() + cleaned.slice(1);
23
+ }
24
+ function commonPrefixPath(files) {
25
+ if (files.length === 0)
26
+ return null;
27
+ const parts = files.map((file) => file.replaceAll("\\", "/").split("/").filter(Boolean));
28
+ const first = parts[0] ?? [];
29
+ const common = [];
30
+ for (let i = 0; i < first.length; i += 1) {
31
+ const value = first[i];
32
+ if (!value)
33
+ break;
34
+ if (parts.every((entry) => entry[i] === value)) {
35
+ common.push(value);
36
+ }
37
+ else {
38
+ break;
39
+ }
40
+ }
41
+ if (common.length === 0)
42
+ return null;
43
+ return `${common.join("/")}/**`;
44
+ }
45
+ function inferScopeFromFiles(dirtyFiles) {
46
+ const normalized = dirtyFiles
47
+ .map((file) => file.trim().replaceAll("\\", "/"))
48
+ .filter((file) => file.length > 0 && !file.startsWith(".agentbridge/") && file !== ".agentbridge");
49
+ if (normalized.length === 0)
50
+ return "**";
51
+ const cfg = (0, config_1.readConfig)();
52
+ const lane = (0, domain_resolution_1.inferLaneFromFiles)(normalized, cfg.domains ?? []);
53
+ if (lane.laneDomain) {
54
+ const domain = (cfg.domains ?? []).find((item) => item.domain === lane.laneDomain);
55
+ if (domain && domain.pathPatterns.length > 0) {
56
+ return domain.pathPatterns[0];
57
+ }
58
+ }
59
+ return commonPrefixPath(normalized) ?? "**";
60
+ }
61
+ function inferWorkContract(input) {
62
+ const prompt = input.prompt?.trim();
63
+ const summary = prompt || humanizeBranchName(input.branchName) || "Continue current work";
64
+ const scope = inferScopeFromFiles(input.dirtyFiles);
65
+ return { summary, scope };
66
+ }
67
+ async function confirmWorkContract(draft, opts = {}) {
68
+ if (opts.nonInteractive) {
69
+ const confirmed = (process.env.AGENTBRIDGE_START_CONFIRM ?? "").trim().toLowerCase();
70
+ if (confirmed !== "yes")
71
+ return null;
72
+ return {
73
+ summary: (process.env.AGENTBRIDGE_START_SUMMARY ?? "").trim() || draft.summary,
74
+ scope: (process.env.AGENTBRIDGE_START_SCOPE ?? "").trim() || draft.scope,
75
+ };
76
+ }
77
+ const rl = (0, promises_1.createInterface)({ input: node_process_1.stdin, output: node_process_1.stdout });
78
+ let current = { ...draft };
79
+ try {
80
+ while (true) {
81
+ node_process_1.stdout.write([
82
+ "",
83
+ "Proposed work contract:",
84
+ `- Summary: ${current.summary}`,
85
+ `- Scope: ${current.scope}`,
86
+ "[C]onfirm [E]dit intent [S]cope [X] cancel",
87
+ "",
88
+ ].join("\n"));
89
+ const answer = (await rl.question("> ")).trim().toLowerCase();
90
+ if (answer === "c" || answer === "confirm" || answer === "")
91
+ return current;
92
+ if (answer === "x" || answer === "cancel")
93
+ return null;
94
+ if (answer === "e" || answer === "edit") {
95
+ const next = (await rl.question("New summary: ")).trim();
96
+ if (next)
97
+ current.summary = next;
98
+ continue;
99
+ }
100
+ if (answer === "s" || answer === "scope") {
101
+ const next = (await rl.question("New scope: ")).trim();
102
+ if (next)
103
+ current.scope = next;
104
+ }
105
+ }
106
+ }
107
+ finally {
108
+ rl.close();
109
+ }
110
+ }