@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,1718 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getDirtyWorkingTreeFiles = void 0;
4
+ exports.renderScopedApprovalConfirmation = renderScopedApprovalConfirmation;
5
+ exports.renderCrossingContext = renderCrossingContext;
6
+ exports.shouldRenderSupervisionSummary = shouldRenderSupervisionSummary;
7
+ exports.syncAndBuildSupervisionSnapshot = syncAndBuildSupervisionSnapshot;
8
+ exports.renderWatchTaskScopeUsage = renderWatchTaskScopeUsage;
9
+ exports.renderWatchStartupHeader = renderWatchStartupHeader;
10
+ exports.buildWatchBlockingIssue = buildWatchBlockingIssue;
11
+ exports.renderWatchBlockingIssue = renderWatchBlockingIssue;
12
+ exports.overlayLocalChangedFilesOnSupervision = overlayLocalChangedFilesOnSupervision;
13
+ exports.isHandoffRequiredCloseError = isHandoffRequiredCloseError;
14
+ exports.renderIdleCloseFailure = renderIdleCloseFailure;
15
+ exports.openOrResumeServerSessionForWatch = openOrResumeServerSessionForWatch;
16
+ exports.normalizeDirtyWorkingTreeFiles = normalizeDirtyWorkingTreeFiles;
17
+ exports.ensureWatchRepoClean = ensureWatchRepoClean;
18
+ exports.runWatch = runWatch;
19
+ const node_fs_1 = require("node:fs");
20
+ const node_path_1 = require("node:path");
21
+ const node_process_1 = require("node:process");
22
+ const promises_1 = require("node:readline/promises");
23
+ const node_child_process_1 = require("node:child_process");
24
+ const config_1 = require("../config");
25
+ const errors_1 = require("../errors");
26
+ const authority_request_1 = require("../authority-request");
27
+ const domain_resolution_1 = require("../domain-resolution");
28
+ const briefing_1 = require("../briefing");
29
+ const session_1 = require("../session");
30
+ const session_state_1 = require("../session-state");
31
+ const watch_core_1 = require("../watch-core");
32
+ const watcher_1 = require("../watcher");
33
+ const server_sync_1 = require("../server-sync");
34
+ const watch_packet_handshake_1 = require("../watch-packet-handshake");
35
+ const work_context_resolver_1 = require("../work-context-resolver");
36
+ const test_runner_1 = require("../test-runner");
37
+ const revert_crossing_1 = require("../revert-crossing");
38
+ const supervision_1 = require("../supervision");
39
+ const http_1 = require("../http");
40
+ const error_catalog_1 = require("../error-catalog");
41
+ const session_state_2 = require("../session-state");
42
+ const file_fingerprints_1 = require("../file-fingerprints");
43
+ const start_1 = require("./start");
44
+ const gates_1 = require("../gates");
45
+ const git_status_1 = require("../git-status");
46
+ Object.defineProperty(exports, "getDirtyWorkingTreeFiles", { enumerable: true, get: function () { return git_status_1.getDirtyWorkingTreeFiles; } });
47
+ const IDLE_CLOSE_MS = 5_000;
48
+ const WATCH_STARTUP_TIMEOUT_MS = 12_000;
49
+ const WATCH_START_TASK_TIMEOUT_MS = 60_000;
50
+ function acceptanceRolloutOptionsFromGates(gates) {
51
+ return {
52
+ rolloutProofTooWeak: gates.rollout_proof_too_weak,
53
+ rolloutProofNotRelevant: gates.rollout_proof_not_relevant,
54
+ rolloutImpactCoverageGap: gates.rollout_impact_coverage_gap,
55
+ };
56
+ }
57
+ function createWatchTimingTracker() {
58
+ const enabled = process.env.AGENTBRIDGE_TIMING === "1";
59
+ const totals = new Map();
60
+ const totalStart = process.hrtime.bigint();
61
+ const add = (phase, elapsedMs) => {
62
+ totals.set(phase, (totals.get(phase) ?? 0) + elapsedMs);
63
+ };
64
+ const nowMs = () => Number(process.hrtime.bigint()) / 1_000_000;
65
+ return {
66
+ trackSync(phase, work) {
67
+ if (!enabled)
68
+ return work();
69
+ const started = nowMs();
70
+ try {
71
+ return work();
72
+ }
73
+ finally {
74
+ add(phase, nowMs() - started);
75
+ }
76
+ },
77
+ async trackAsync(phase, work) {
78
+ if (!enabled)
79
+ return work();
80
+ const started = nowMs();
81
+ try {
82
+ return await work();
83
+ }
84
+ finally {
85
+ add(phase, nowMs() - started);
86
+ }
87
+ },
88
+ flushTotal() {
89
+ if (!enabled)
90
+ return;
91
+ const totalMs = Number(process.hrtime.bigint() - totalStart) / 1_000_000;
92
+ add("total", totalMs);
93
+ for (const [phase, elapsed] of totals.entries()) {
94
+ process.stdout.write(`[agentbridge:timing] ${phase}=${Math.round(elapsed)}ms\n`);
95
+ }
96
+ },
97
+ };
98
+ }
99
+ function renderScopedApprovalConfirmation(agent, file, domain) {
100
+ return `Approved scoped crossing: ${agent} may modify ${file} for this session only. No wider ${domain ?? "target"} authority granted.`;
101
+ }
102
+ function renderCrossingContext(input) {
103
+ return [
104
+ "Crossing context:",
105
+ `- current task: ${input.changeRequestId ?? "unknown"}`,
106
+ `- active run: ${input.workSessionId}`,
107
+ `- worker: ${input.worker}`,
108
+ `- claimed lane/domain: ${input.claimedLaneDomain ?? "unclassified"}`,
109
+ `- touched domain: ${input.touchedDomain ?? "unknown"}`,
110
+ `- file: ${input.file}`,
111
+ `- tier: ${input.tier}`,
112
+ "- required action: approve, handoff, limit, deny, abandon, or revert",
113
+ ].join("\n");
114
+ }
115
+ function shouldRenderSupervisionSummary(lastSignature, snapshot) {
116
+ const nextSignature = (0, supervision_1.supervisionSignature)(snapshot);
117
+ return {
118
+ shouldRender: nextSignature !== lastSignature,
119
+ nextSignature,
120
+ };
121
+ }
122
+ async function syncAndBuildSupervisionSnapshot(input) {
123
+ const { ctx, state, cfgDomains, changedFile, outcome, changeRequestId, unresolvedProtectedCrossing, rollout, timing, } = input;
124
+ const contextError = (err) => (err instanceof Error ? err.message : String(err));
125
+ if (!state.serverSessionId || !ctx) {
126
+ return {
127
+ supervision: (0, supervision_1.fallbackSupervisionSnapshot)({
128
+ workSessionId: state.serverSessionId ?? state.id,
129
+ changeRequestId,
130
+ changedFiles: [...state.changedFiles],
131
+ domains: cfgDomains,
132
+ unresolvedProtectedCrossing,
133
+ blocked: state.status === "blocked",
134
+ }),
135
+ acceptanceReport: null,
136
+ };
137
+ }
138
+ const serverSessionId = state.serverSessionId;
139
+ let syncError;
140
+ const syncDiff = () => (0, server_sync_1.postObservedDiff)(ctx, { workSessionId: serverSessionId }, {
141
+ activeLaneDomain: state.laneDomain ?? null,
142
+ sourceHead: (() => {
143
+ try {
144
+ return (0, node_child_process_1.execFileSync)("git", ["rev-parse", "HEAD"], {
145
+ stdio: ["ignore", "pipe", "ignore"],
146
+ })
147
+ .toString("utf8")
148
+ .trim();
149
+ }
150
+ catch {
151
+ return undefined;
152
+ }
153
+ })(),
154
+ changedFiles: [...state.changedFiles],
155
+ fileFingerprints: (0, file_fingerprints_1.computeFileFingerprints)(state.changedFiles),
156
+ changedFileEntries: [
157
+ {
158
+ path: changedFile,
159
+ status: "modified",
160
+ owningDomain: outcome.crossingDomain ?? state.laneDomain ?? null,
161
+ activeLaneDomain: state.laneDomain ?? null,
162
+ crossing: Boolean(outcome.crossingDomain) && outcome.crossingDomain !== state.laneDomain,
163
+ severity: outcome.crossingTier,
164
+ },
165
+ ],
166
+ boundaryCrossings: outcome.crossingDomain && state.laneDomain && outcome.crossingDomain !== state.laneDomain
167
+ ? [
168
+ {
169
+ file: changedFile,
170
+ fromDomain: state.laneDomain,
171
+ toDomain: outcome.crossingDomain,
172
+ severity: outcome.crossingTier,
173
+ },
174
+ ]
175
+ : [],
176
+ });
177
+ try {
178
+ if (timing) {
179
+ await timing.trackAsync("observed_diff_sync", syncDiff);
180
+ }
181
+ else {
182
+ await syncDiff();
183
+ }
184
+ }
185
+ catch (err) {
186
+ syncError = contextError(err);
187
+ }
188
+ const fetchAcceptance = () => (0, server_sync_1.fetchAcceptanceCheck)(ctx, { workSessionId: serverSessionId, ...rollout });
189
+ try {
190
+ const report = timing
191
+ ? await timing.trackAsync("acceptance_check_fetch", fetchAcceptance)
192
+ : await fetchAcceptance();
193
+ return {
194
+ supervision: overlayLocalChangedFilesOnSupervision((0, supervision_1.supervisionFromAcceptance)(report, cfgDomains), state.changedFiles),
195
+ syncError,
196
+ acceptanceReport: report,
197
+ };
198
+ }
199
+ catch (err) {
200
+ return {
201
+ supervision: (0, supervision_1.fallbackSupervisionSnapshot)({
202
+ workSessionId: state.serverSessionId ?? state.id,
203
+ changeRequestId,
204
+ changedFiles: [...state.changedFiles],
205
+ domains: cfgDomains,
206
+ unresolvedProtectedCrossing,
207
+ blocked: state.status === "blocked",
208
+ serverAcceptanceUnavailable: contextError(err),
209
+ }),
210
+ syncError,
211
+ acceptanceReport: null,
212
+ };
213
+ }
214
+ }
215
+ function normalizeInline(value) {
216
+ return value.trim().replaceAll("\\", "/");
217
+ }
218
+ function normalizeTaskSummary(value) {
219
+ return value.trim().replace(/\s+/g, " ").toLowerCase();
220
+ }
221
+ function isActiveLocalSession(state) {
222
+ return Boolean(state && state.status !== "closed" && state.id !== "none");
223
+ }
224
+ function renderWatchTaskScopeUsage() {
225
+ return [
226
+ "Usage:",
227
+ " agentbridge watch",
228
+ ].join("\n");
229
+ }
230
+ function parseTaskScopeInput(options) {
231
+ const task = options.task?.trim();
232
+ const scope = options.scope?.trim();
233
+ return {
234
+ ...(task ? { task } : {}),
235
+ ...(scope ? { scope } : {}),
236
+ };
237
+ }
238
+ function inferTaskFromFiles(files, domains) {
239
+ if (files.length === 0)
240
+ return "Inferred from changed files/current work";
241
+ const byDomain = new Map();
242
+ let docsOnly = true;
243
+ for (const file of files) {
244
+ const resolved = (0, domain_resolution_1.resolveDomainForFile)(file, domains).domain ?? "Unknown";
245
+ byDomain.set(resolved, (byDomain.get(resolved) ?? 0) + 1);
246
+ if (!file.startsWith("docs/"))
247
+ docsOnly = false;
248
+ }
249
+ if (docsOnly)
250
+ return "Editing docs files";
251
+ if (byDomain.size === 1) {
252
+ const [domain] = [...byDomain.keys()];
253
+ if (domain === "Unknown")
254
+ return "Unknown task";
255
+ return `Working in ${domain} domain`;
256
+ }
257
+ return "Inferred from changed files/current work";
258
+ }
259
+ function renderBrainstormingStatus() {
260
+ return [
261
+ "No coding changes detected yet.",
262
+ "Mode: brainstorming/planning",
263
+ "Next: When your agent starts changing files, AgentBridge will review proof/scope risk.",
264
+ "",
265
+ ].join("\n");
266
+ }
267
+ function renderCodingDetection(changedFiles, inferredTask) {
268
+ return [
269
+ "AgentBridge detected coding work.",
270
+ `Task: ${inferredTask}`,
271
+ "Mode: coding/implementation",
272
+ "Changed files:",
273
+ ...changedFiles.map((file) => `- ${file}`),
274
+ "",
275
+ ].join("\n");
276
+ }
277
+ function detectInferredDomainDrift(files, domains) {
278
+ if (files.length <= 1)
279
+ return null;
280
+ const touched = new Map();
281
+ for (const file of files) {
282
+ const resolved = (0, domain_resolution_1.resolveDomainForFile)(file, domains);
283
+ const domain = resolved.domain ?? "Unknown";
284
+ const tier = resolved.tier ?? "tier_d";
285
+ const existing = touched.get(domain);
286
+ touched.set(domain, existing ? { count: existing.count + 1, tier: existing.tier } : { count: 1, tier });
287
+ }
288
+ if (touched.size <= 1)
289
+ return null;
290
+ const highRisk = [...touched.values()].some((entry) => entry.tier === "tier_a" || entry.tier === "tier_b");
291
+ const changedFiles = [...files];
292
+ const issue = {
293
+ errorCode: "POSSIBLE_DOMAIN_DRIFT",
294
+ whatHappened: "Changes span multiple project areas without an explicit scope.",
295
+ whyItMatters: highRisk
296
+ ? "Cross-domain changes touching protected areas are high-risk without clear scoping."
297
+ : "Multi-area edits can hide accidental drift and make review harder.",
298
+ files: changedFiles,
299
+ suggestedPrompt: [
300
+ "AgentBridge noticed changes across multiple project areas.",
301
+ "Explain why these files belong to the same task, or split the work into separate steps.",
302
+ `Changed files: ${changedFiles.join(", ")}`,
303
+ ].join("\n"),
304
+ nextAction: highRisk
305
+ ? "Explain/split the work, then rerun watch."
306
+ : "Review intent for each area; split if needed.",
307
+ };
308
+ return { issue, blocking: highRisk };
309
+ }
310
+ async function resolveCurrentSessionTaskSummary(state) {
311
+ if (!state.changeRequestId)
312
+ return null;
313
+ try {
314
+ const cr = await (0, server_sync_1.getChangeRequest)((0, config_1.contextFromConfig)(), state.changeRequestId);
315
+ return cr.title?.trim() || null;
316
+ }
317
+ catch {
318
+ return null;
319
+ }
320
+ }
321
+ function renderWatchStartupHeader(input) {
322
+ const modeLine = input.mode === "explicit"
323
+ ? "Mode: coding/implementation (scoped policy active)"
324
+ : "Mode: inferred (brainstorming/planning or coding/implementation)";
325
+ return [
326
+ "AgentBridge is watching",
327
+ modeLine,
328
+ `Task: ${input.task}`,
329
+ ...(input.scope ? [`Scope: ${input.scope}`] : []),
330
+ `Current status: ${input.currentStatus}`,
331
+ `Next guidance: ${input.nextGuidance}`,
332
+ "",
333
+ ].join("\n");
334
+ }
335
+ function renderStartupPhase(step, status) {
336
+ const verb = status === "starting" ? "starting" : status === "done" ? "done" : "skipped";
337
+ return `[startup] ${step}: ${verb}\n`;
338
+ }
339
+ async function withTimeout(work, timeoutMs, onTimeout) {
340
+ let timer = null;
341
+ try {
342
+ return await Promise.race([
343
+ work,
344
+ new Promise((_resolve, reject) => {
345
+ timer = setTimeout(() => reject(onTimeout()), timeoutMs);
346
+ }),
347
+ ]);
348
+ }
349
+ finally {
350
+ if (timer)
351
+ clearTimeout(timer);
352
+ }
353
+ }
354
+ async function flushStdout() {
355
+ if (process.stdout.writableNeedDrain) {
356
+ await new Promise((resolveDrain) => process.stdout.once("drain", resolveDrain));
357
+ }
358
+ await new Promise((resolveTick) => setImmediate(resolveTick));
359
+ }
360
+ function buildWatchBlockingIssue(report, options) {
361
+ if (options.strictScope && (report.out_of_scope_files ?? []).length > 0) {
362
+ const files = [...new Set(report.out_of_scope_files ?? [])];
363
+ return {
364
+ errorCode: "SCOPE_DRIFT_OUT_OF_SCOPE_FILE",
365
+ whatHappened: "Files were changed outside the declared scope.",
366
+ whyItMatters: "Out-of-scope edits can invalidate task boundaries and watch results.",
367
+ files,
368
+ suggestedPrompt: [
369
+ "You changed files outside the allowed scope.",
370
+ "Revert them, justify why they are required and ask to expand scope, or split them into a separate task.",
371
+ `Out-of-scope files: ${files.join(", ")}`,
372
+ ].join("\n"),
373
+ nextAction: "Revert out-of-scope files, then run: agentbridge check",
374
+ };
375
+ }
376
+ if (report.decision === "stale_evidence") {
377
+ const staleFiles = [...new Set(report.stale_files ?? report.evidence_missing ?? report.changed_files)];
378
+ return {
379
+ errorCode: "PROOF_STALE_AFTER_CHANGE",
380
+ whatHappened: "Verification proof is stale because files changed after the last passing verify.",
381
+ whyItMatters: "Acceptance needs proof that matches the final edited files.",
382
+ files: staleFiles,
383
+ suggestedPrompt: [
384
+ "Your proof is stale because files changed after verification.",
385
+ "Rerun verification after your final edit, then rerun AgentBridge watch.",
386
+ `Files: ${staleFiles.join(", ")}`,
387
+ ].join("\n"),
388
+ nextAction: "Run a verification command for the final changed files.",
389
+ };
390
+ }
391
+ if (report.decision === "failed") {
392
+ return {
393
+ errorCode: "VERIFICATION_FAILED",
394
+ whatHappened: "verification failed",
395
+ whyItMatters: "Failed verification cannot be used as acceptance proof.",
396
+ files: [...new Set(report.changed_files)],
397
+ suggestedPrompt: [
398
+ "The last verification command failed.",
399
+ "Please fix the failure and rerun verification with a passing result before handoff.",
400
+ ].join("\n"),
401
+ nextAction: "Fix verification failures, then rerun verification on final files.",
402
+ };
403
+ }
404
+ if (report.decision === "needs_proof") {
405
+ const files = [...new Set(report.changed_files)];
406
+ return {
407
+ errorCode: "PROOF_MISSING",
408
+ whatHappened: "No valid verification proof is attached to the current work.",
409
+ whyItMatters: "Work cannot be accepted without fresh proof for the current scope.",
410
+ files,
411
+ suggestedPrompt: [
412
+ "Your work is not ready. AgentBridge found changed files with no proof attached.",
413
+ "Run verification for the changed files, then rerun AgentBridge watch.",
414
+ `Files: ${files.join(", ") || "current files"}`,
415
+ ].join("\n"),
416
+ nextAction: "Run a verification command for the changed files.",
417
+ };
418
+ }
419
+ return null;
420
+ }
421
+ function buildFallbackWatchBlockingIssue(supervision) {
422
+ if (supervision.decision === "needs_proof") {
423
+ return {
424
+ errorCode: "PROOF_MISSING",
425
+ whatHappened: "No valid verification proof is attached to the current work.",
426
+ whyItMatters: "Work cannot be accepted without fresh proof for the current scope.",
427
+ files: [],
428
+ suggestedPrompt: [
429
+ "Your work is not ready. AgentBridge found changed files with no proof attached.",
430
+ "Run verification for the changed files, then rerun AgentBridge watch.",
431
+ `Files: ${supervision.changedFiles.length > 0 ? supervision.changedFiles.join(", ") : "current files"}`,
432
+ ].join("\n"),
433
+ nextAction: "Run a verification command for the changed files.",
434
+ };
435
+ }
436
+ if (supervision.decision === "failed") {
437
+ return {
438
+ errorCode: "VERIFICATION_FAILED",
439
+ whatHappened: "Current task run is blocked and cannot complete.",
440
+ whyItMatters: "Blocked runs must be resolved before the task can be marked done.",
441
+ files: supervision.changedFiles,
442
+ suggestedPrompt: [
443
+ "Resolve the problem in this run before completing the task.",
444
+ `Files in current blocked state: ${supervision.changedFiles.join(", ") || "none listed"}`,
445
+ ].join("\n"),
446
+ nextAction: "Resolve proof/scope problems, then rerun watch.",
447
+ };
448
+ }
449
+ return null;
450
+ }
451
+ function renderWatchBlockingIssue(issue) {
452
+ const filesLine = issue.files.length > 0 ? issue.files.join(", ") : "none listed";
453
+ return [
454
+ "AgentBridge caught an issue",
455
+ `What happened: ${issue.whatHappened}`,
456
+ `Why it matters: ${issue.whyItMatters}`,
457
+ `Files: ${filesLine}`,
458
+ `Error code: ${issue.errorCode}`,
459
+ "Suggested prompt to send back to agent:",
460
+ "```text",
461
+ issue.suggestedPrompt,
462
+ "```",
463
+ `Next action: ${issue.nextAction}`,
464
+ "",
465
+ ].join("\n");
466
+ }
467
+ function overlayLocalChangedFilesOnSupervision(supervision, localChangedFiles) {
468
+ if (supervision.changedFiles.length > 0 || localChangedFiles.length === 0) {
469
+ return supervision;
470
+ }
471
+ const nextChangedFiles = [...new Set(localChangedFiles)].sort();
472
+ const nextWorkType = supervision.workType === "unknown" ? (0, supervision_1.inferSupervisionWorkType)(nextChangedFiles) : supervision.workType;
473
+ return {
474
+ ...supervision,
475
+ changedFiles: nextChangedFiles,
476
+ workType: nextWorkType,
477
+ };
478
+ }
479
+ function isCrAlreadyInSessionError(err) {
480
+ if (!(err instanceof http_1.CliHttpError) || err.status !== 422)
481
+ return false;
482
+ return /current:\s*in_session/i.test(err.body) || /in_session/i.test(err.body);
483
+ }
484
+ function isHandoffRequiredCloseError(err) {
485
+ if (!(err instanceof http_1.CliHttpError))
486
+ return false;
487
+ if (![400, 409, 422].includes(err.status))
488
+ return false;
489
+ try {
490
+ const parsed = JSON.parse(err.body);
491
+ const text = `${String(parsed.code ?? "")} ${String(parsed.error ?? "")} ${String(parsed.message ?? "")}`.toLowerCase();
492
+ return text.includes("handoff_required");
493
+ }
494
+ catch {
495
+ return err.message.toLowerCase().includes("handoff_required");
496
+ }
497
+ }
498
+ function renderIdleCloseFailure(err) {
499
+ if (isHandoffRequiredCloseError(err)) {
500
+ return "Close pending: completion summary required before the tracked session can close.\nNext: provide the completion summary and continue supervision.";
501
+ }
502
+ return `Server close sync failed: ${String(err)}`;
503
+ }
504
+ async function openOrResumeServerSessionForWatch(input) {
505
+ const { ctx, changeRequestId, executionSurfaceId, file, activeAgentId, inferredLaneDomain } = input;
506
+ try {
507
+ const serverSession = await (0, server_sync_1.openServerSession)(ctx, {
508
+ changeRequestId,
509
+ executionSurfaceId,
510
+ intent: "Watched local work session",
511
+ claimedPaths: [file],
512
+ activeAgentId,
513
+ inferredLaneDomain,
514
+ });
515
+ return { workSessionId: serverSession.workSessionId, resumed: false };
516
+ }
517
+ catch (err) {
518
+ if (!isCrAlreadyInSessionError(err))
519
+ throw err;
520
+ const resolved = await (0, work_context_resolver_1.findSingleActiveSessionForChangeRequest)(ctx, changeRequestId);
521
+ if (!resolved)
522
+ return null;
523
+ if ("ambiguous" in resolved) {
524
+ process.stdout.write(`Multiple active runs for task ${changeRequestId}; cannot resume automatically.\n` +
525
+ `${resolved.ambiguous.map((s) => ` - ${s.id} (${s.status})`).join("\n")}\n` +
526
+ "Use agentbridge session list and session abandon to reduce to one.\n");
527
+ return null;
528
+ }
529
+ return { workSessionId: resolved.workSessionId, resumed: true };
530
+ }
531
+ }
532
+ async function promptDecision() {
533
+ const rl = (0, promises_1.createInterface)({ input: process.stdin, output: process.stdout });
534
+ try {
535
+ while (true) {
536
+ const value = (await rl.question("Decision [a/d/l/h/x]: ")).trim().toLowerCase();
537
+ if (value === "a")
538
+ return { decision: "approve" };
539
+ if (value === "d")
540
+ return { decision: "deny" };
541
+ if (value === "l") {
542
+ const narrowed = (await rl.question("Limit scope: [1] file-only [2] read-only domain [3] deny all writes\n> "))
543
+ .trim()
544
+ .toLowerCase();
545
+ if (narrowed === "2" || narrowed === "3") {
546
+ return { decision: "limit_scope", limitMode: "read_only" };
547
+ }
548
+ return { decision: "limit_scope", limitMode: "file_only" };
549
+ }
550
+ if (value === "h")
551
+ return { decision: "handoff" };
552
+ if (value === "x")
553
+ return { decision: "abandon" };
554
+ }
555
+ }
556
+ finally {
557
+ rl.close();
558
+ }
559
+ }
560
+ function normalizeDirtyFilePath(file) {
561
+ const trimmed = file.trim();
562
+ if (!trimmed)
563
+ return "";
564
+ // For rename entries in porcelain output, keep the destination path.
565
+ const renamed = trimmed.includes(" -> ") ? trimmed.split(" -> ").at(-1) ?? trimmed : trimmed;
566
+ return renamed.replaceAll("\\", "/").replace(/^\.\/+/, "");
567
+ }
568
+ function normalizeDirtyWorkingTreeFiles(files) {
569
+ const expandDirectoryToFiles = (path) => {
570
+ const absolute = (0, node_path_1.resolve)((0, node_process_1.cwd)(), path);
571
+ try {
572
+ const stats = (0, node_fs_1.statSync)(absolute);
573
+ if (!stats.isDirectory())
574
+ return [path];
575
+ }
576
+ catch {
577
+ return [path];
578
+ }
579
+ const collected = [];
580
+ const stack = [absolute];
581
+ while (stack.length > 0) {
582
+ const current = stack.pop();
583
+ if (!current)
584
+ continue;
585
+ let entries;
586
+ try {
587
+ entries = (0, node_fs_1.readdirSync)(current, { withFileTypes: true, encoding: "utf8" });
588
+ }
589
+ catch {
590
+ continue;
591
+ }
592
+ for (const entry of entries) {
593
+ const child = (0, node_path_1.resolve)(current, entry.name);
594
+ if (entry.isDirectory()) {
595
+ stack.push(child);
596
+ continue;
597
+ }
598
+ if (!entry.isFile())
599
+ continue;
600
+ const rel = (0, node_path_1.relative)((0, node_process_1.cwd)(), child).replaceAll("\\", "/");
601
+ if (rel && !rel.startsWith(".."))
602
+ collected.push(rel);
603
+ }
604
+ }
605
+ if (collected.length === 0)
606
+ return [path];
607
+ return collected.sort();
608
+ };
609
+ const seen = new Set();
610
+ const ordered = [];
611
+ for (const raw of files) {
612
+ const normalized = normalizeDirtyFilePath(raw);
613
+ if (!normalized || normalized.startsWith(".."))
614
+ continue;
615
+ if (normalized === ".agentbridge" || normalized.startsWith(".agentbridge/"))
616
+ continue;
617
+ const expanded = expandDirectoryToFiles(normalized);
618
+ for (const candidate of expanded) {
619
+ const flat = normalizeDirtyFilePath(candidate);
620
+ if (!flat || flat.startsWith(".."))
621
+ continue;
622
+ if (flat === ".agentbridge" || flat.startsWith(".agentbridge/"))
623
+ continue;
624
+ if (seen.has(flat))
625
+ continue;
626
+ seen.add(flat);
627
+ ordered.push(flat);
628
+ }
629
+ }
630
+ return ordered;
631
+ }
632
+ function filterIgnoredDirtyFiles(files, ignored) {
633
+ if (ignored.size === 0)
634
+ return files;
635
+ return files.filter((file) => !ignored.has(file.replaceAll("\\", "/")));
636
+ }
637
+ function fileWithinScopedPaths(file, scopedPaths) {
638
+ if (scopedPaths.length === 0)
639
+ return false;
640
+ return scopedPaths.some((claim) => {
641
+ const normalized = claim.trim().replaceAll("\\", "/");
642
+ if (!normalized)
643
+ return false;
644
+ if (normalized === file)
645
+ return true;
646
+ if (normalized.endsWith("/**")) {
647
+ const prefix = normalized.slice(0, -3);
648
+ return file.startsWith(prefix);
649
+ }
650
+ if (normalized.includes("*")) {
651
+ const withDouble = normalized.replaceAll("**", "__DOUBLE_STAR__");
652
+ const withSingle = withDouble.replaceAll("*", "__SINGLE_STAR__");
653
+ const escaped = withSingle
654
+ .replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
655
+ .replaceAll("__DOUBLE_STAR__", ".*")
656
+ .replaceAll("__SINGLE_STAR__", "[^/]*");
657
+ return new RegExp(`^${escaped}$`).test(file);
658
+ }
659
+ return file.startsWith(normalized.endsWith("/") ? normalized : `${normalized}/`);
660
+ });
661
+ }
662
+ function startupDirtyClassification(input) {
663
+ const workspaceDirtySnapshot = [...new Set(input.dirtyFiles)].sort();
664
+ const localChanged = new Set(input.localState?.changedFiles ?? []);
665
+ const localClaimedPaths = [...new Set(input.localState?.claimedPaths ?? [])]
666
+ .map((claim) => claim.trim())
667
+ .filter(Boolean);
668
+ const lanePatterns = input.localState?.laneDomain != null
669
+ ? (input.domains.find((domain) => domain.domain === input.localState?.laneDomain)?.pathPatterns ?? [])
670
+ : [];
671
+ const claimedPaths = [...new Set([...input.claimedPaths, ...localClaimedPaths])]
672
+ .map((claim) => claim.trim())
673
+ .filter(Boolean);
674
+ const scopeProven = claimedPaths.length > 0 || lanePatterns.length > 0 || (input.localState?.changedFiles.length ?? 0) > 0;
675
+ if (input.inferredMode) {
676
+ return {
677
+ acceptanceCandidateFiles: workspaceDirtySnapshot,
678
+ workspaceDirtySnapshot,
679
+ unscopedStartupDirty: [],
680
+ scopeProven: workspaceDirtySnapshot.length > 0,
681
+ };
682
+ }
683
+ const acceptanceCandidateFiles = workspaceDirtySnapshot.filter((file) => {
684
+ if (localChanged.has(file))
685
+ return true;
686
+ if (claimedPaths.length > 0) {
687
+ return fileWithinScopedPaths(file, claimedPaths);
688
+ }
689
+ if (fileWithinScopedPaths(file, lanePatterns))
690
+ return true;
691
+ return false;
692
+ });
693
+ const scopedSet = new Set(acceptanceCandidateFiles);
694
+ const unscopedStartupDirty = workspaceDirtySnapshot.filter((file) => !scopedSet.has(file));
695
+ return {
696
+ acceptanceCandidateFiles,
697
+ workspaceDirtySnapshot,
698
+ unscopedStartupDirty,
699
+ scopeProven,
700
+ };
701
+ }
702
+ function mtimeMsForFile(file) {
703
+ try {
704
+ return (0, node_fs_1.statSync)((0, node_path_1.resolve)((0, node_process_1.cwd)(), file)).mtimeMs;
705
+ }
706
+ catch {
707
+ return null;
708
+ }
709
+ }
710
+ function buildStartupScopeDriftIssue(input) {
711
+ const { unscopedStartupDirty, acceptanceCandidateFiles, claimedPaths } = input;
712
+ if (unscopedStartupDirty.length === 0)
713
+ return null;
714
+ const scopeAnchors = claimedPaths
715
+ .map((claim) => normalizeInline(claim))
716
+ .filter((claim) => claim.length > 0)
717
+ .map((claim) => {
718
+ if (claim.endsWith("/**"))
719
+ return claim.slice(0, -3);
720
+ if (claim.includes("*"))
721
+ return claim.split("*")[0] ?? claim;
722
+ if (claim.endsWith("/"))
723
+ return claim.slice(0, -1);
724
+ const slashIdx = claim.lastIndexOf("/");
725
+ return slashIdx >= 0 ? claim.slice(0, slashIdx) : claim;
726
+ })
727
+ .filter((anchor) => anchor.length > 0);
728
+ const scopeFileTimes = acceptanceCandidateFiles
729
+ .map((file) => mtimeMsForFile(file))
730
+ .filter((mtime) => typeof mtime === "number");
731
+ const referenceMtime = scopeFileTimes.length > 0 ? Math.max(...scopeFileTimes) : null;
732
+ const RECENT_WINDOW_MS = 10 * 1000;
733
+ const likelyCurrentTaskFiles = unscopedStartupDirty.filter((file) => {
734
+ const inScopeNeighborhood = scopeAnchors.length === 0 ||
735
+ scopeAnchors.some((anchor) => file === anchor || file.startsWith(`${anchor}/`));
736
+ if (!inScopeNeighborhood)
737
+ return false;
738
+ const mtime = mtimeMsForFile(file);
739
+ if (referenceMtime == null)
740
+ return true;
741
+ if (mtime == null)
742
+ return true;
743
+ return Math.abs(referenceMtime - mtime) <= RECENT_WINDOW_MS;
744
+ });
745
+ if (likelyCurrentTaskFiles.length === 0 && acceptanceCandidateFiles.length > 0) {
746
+ // Keep ambient unrelated dirt in debug output only.
747
+ return null;
748
+ }
749
+ const files = likelyCurrentTaskFiles.length > 0 ? likelyCurrentTaskFiles : unscopedStartupDirty;
750
+ return {
751
+ errorCode: "SCOPE_DRIFT_OUT_OF_SCOPE_FILE",
752
+ whatHappened: "Outside promised scope.",
753
+ whyItMatters: "Out-of-scope edits break the declared task boundary and block acceptance.",
754
+ files,
755
+ suggestedPrompt: [
756
+ "You changed files outside the allowed scope.",
757
+ "Revert them, justify why they are required and ask to expand scope, or split them into a separate task.",
758
+ `Out-of-scope files: ${files.join(", ")}`,
759
+ ].join("\n"),
760
+ nextAction: "Revert out-of-scope files (or explicitly expand scope), then rerun watch/check.",
761
+ };
762
+ }
763
+ async function ensureWatchRepoClean(allowDirty = false, ignoredDirtyFiles = new Set()) {
764
+ if (allowDirty)
765
+ return;
766
+ const dirtyFiles = filterIgnoredDirtyFiles((0, git_status_1.getDirtyWorkingTreeFiles)(), ignoredDirtyFiles);
767
+ if (dirtyFiles.length === 0)
768
+ return;
769
+ const rl = (0, promises_1.createInterface)({ input: process.stdin, output: process.stdout });
770
+ try {
771
+ process.stdout.write("This repo already has uncommitted changes. AgentBridge may treat them as part of the session.\n");
772
+ while (true) {
773
+ const answer = (await rl.question("[c] continue anyway [s] show dirty files [q] quit\n> "))
774
+ .trim()
775
+ .toLowerCase();
776
+ if (answer === "c")
777
+ return;
778
+ if (answer === "s") {
779
+ process.stdout.write(`Dirty files:\n${dirtyFiles.map((f) => `- ${f}`).join("\n")}\n`);
780
+ continue;
781
+ }
782
+ if (answer === "q" || answer === "") {
783
+ throw new errors_1.SafeCliError("Watch cancelled due to pre-existing dirty working tree. Use --allow-dirty to bypass.");
784
+ }
785
+ }
786
+ }
787
+ finally {
788
+ rl.close();
789
+ }
790
+ }
791
+ function currentGitHead() {
792
+ try {
793
+ const head = (0, node_child_process_1.execFileSync)("git", ["rev-parse", "HEAD"], {
794
+ stdio: ["ignore", "pipe", "ignore"],
795
+ })
796
+ .toString("utf8")
797
+ .trim();
798
+ return head.length > 0 ? head : null;
799
+ }
800
+ catch {
801
+ return null;
802
+ }
803
+ }
804
+ /**
805
+ * One-shot Agent Watch fast path. Collapses cold-create / resume + observed-diff
806
+ * sync + acceptance check into a single bounded server call, then renders the same
807
+ * supervision summary + blocking issues the legacy multi-call path produces.
808
+ *
809
+ * Returns `{ handled: false }` when the connected server predates the
810
+ * `/watch/review-once` endpoint so the caller falls back to the legacy flow.
811
+ * Correctness is preserved: proof/scope/session checks all run server-side and
812
+ * the local session is left active for subsequent verify/follow-up.
813
+ */
814
+ async function runWatchOnceFastPath(input) {
815
+ const { cfg, activeAgentId, task, scope, changeRequestId, allowDirty, timing } = input;
816
+ const ignoredDirtyFiles = input.ignoredDirtyFiles ?? new Set();
817
+ let ctx;
818
+ try {
819
+ ctx = (0, config_1.contextFromConfig)();
820
+ }
821
+ catch {
822
+ // No server context (offline / unconfigured) — defer to legacy local flow.
823
+ return { handled: false, hadBlockingIssue: false };
824
+ }
825
+ const dirtyFiles = timing.trackSync("git_snapshot", () => normalizeDirtyWorkingTreeFiles(filterIgnoredDirtyFiles((0, git_status_1.getDirtyWorkingTreeFiles)(), ignoredDirtyFiles)));
826
+ const localState = (0, session_state_1.readSessionState)();
827
+ const claimedPaths = [
828
+ ...new Set([scope, ...(localState?.claimedPaths ?? [])].map((path) => path.trim()).filter(Boolean)),
829
+ ];
830
+ const classification = timing.trackSync("baseline_classification", () => startupDirtyClassification({
831
+ dirtyFiles,
832
+ localState,
833
+ claimedPaths,
834
+ domains: cfg.domains ?? [],
835
+ inferredMode: false,
836
+ }));
837
+ const changedFiles = classification.workspaceDirtySnapshot;
838
+ const fingerprints = timing.trackSync("file_fingerprints", () => (0, file_fingerprints_1.computeFileFingerprints)(changedFiles));
839
+ const sourceHead = timing.trackSync("git_head", () => currentGitHead());
840
+ let result;
841
+ try {
842
+ result = await timing.trackAsync("watch_review_once", async () => withTimeout((0, server_sync_1.reviewOnceForWatch)(ctx, {
843
+ taskSummary: task,
844
+ scope,
845
+ domain: null,
846
+ localSessionId: localState?.serverSessionId ?? null,
847
+ localChangeRequestId: changeRequestId,
848
+ changedFiles,
849
+ scopedFiles: classification.acceptanceCandidateFiles,
850
+ outOfScopeFiles: classification.unscopedStartupDirty,
851
+ fileFingerprints: fingerprints,
852
+ sourceHead,
853
+ allowDirty,
854
+ }), WATCH_START_TASK_TIMEOUT_MS, () => new errors_1.SafeCliError({
855
+ code: "WATCH_STARTUP_TIMEOUT",
856
+ category: "WATCH_ERROR",
857
+ what: "Watch startup timed out while running the one-shot review.",
858
+ why: "The server did not return the consolidated review in time.",
859
+ next: "Run `agentbridge doctor`, then retry `agentbridge start`.",
860
+ suggestedPrompt: "AgentBridge could not start watching this task quickly. Run doctor, confirm project access, then retry.",
861
+ })));
862
+ }
863
+ catch (err) {
864
+ if ((0, server_sync_1.isWatchFastPathUnavailable)(err)) {
865
+ // Older server build without the consolidated endpoint — fall back.
866
+ return { handled: false, hadBlockingIssue: false };
867
+ }
868
+ // WS2: Hard session binding — server rejected our local_session_id because
869
+ // the scope/task doesn't match (WORK_CONTEXT_SCOPE_MISMATCH) or the session
870
+ // no longer exists (WORK_CONTEXT_STALE_LOCAL_SESSION). These are hard-stops:
871
+ // clear the stale local session so the next invocation starts clean, then
872
+ // surface the structured catalog error instead of crashing with a raw throw.
873
+ if ((0, http_1.isCliHttpError)(err)) {
874
+ const parsed = (0, http_1.parseCliHttpErrorBody)(err);
875
+ const rawCode = parsed?.["code"] ?? parsed?.["error"] ?? "";
876
+ const catalogCode = (0, error_catalog_1.mapServerCodeToCatalog)(rawCode);
877
+ const SESSION_BINDING_ERRORS = new Set([
878
+ "WORK_CONTEXT_SCOPE_MISMATCH",
879
+ "WORK_CONTEXT_STALE_LOCAL_SESSION",
880
+ "WORK_CONTEXT_MISSING",
881
+ "WORK_CONTEXT_AMBIGUOUS_SESSIONS",
882
+ ]);
883
+ if (catalogCode && SESSION_BINDING_ERRORS.has(catalogCode)) {
884
+ // Drop the stale local session so the user/agent doesn't keep hitting it.
885
+ (0, session_state_2.clearSessionState)();
886
+ const entry = (0, error_catalog_1.catalogEntryForCode)(catalogCode);
887
+ throw new errors_1.SafeCliError({
888
+ code: entry?.code ?? catalogCode,
889
+ category: entry?.category ?? "WORK_CONTEXT_ERROR",
890
+ what: entry?.what ?? err.message,
891
+ why: entry?.why ?? "The server rejected the local session binding.",
892
+ next: entry?.next ?? 'Run `agentbridge start` to create a new session.',
893
+ suggestedPrompt: "AgentBridge found a different active task/scope. Do not continue on the wrong task. Clear or resolve the current run, then start the intended task again.",
894
+ });
895
+ }
896
+ }
897
+ throw err;
898
+ }
899
+ // Persist a local session bound to the server session so verify/follow-up
900
+ // works and the session stays alive after this one-shot run.
901
+ const lane = (0, domain_resolution_1.inferLaneFromFiles)(classification.acceptanceCandidateFiles.length > 0
902
+ ? classification.acceptanceCandidateFiles
903
+ : changedFiles, cfg.domains ?? []);
904
+ const persistedClaimed = result.claimed_paths.length > 0 ? result.claimed_paths : claimedPaths;
905
+ (0, session_1.openLocalSession)({
906
+ agentId: activeAgentId,
907
+ laneDomain: lane.laneDomain,
908
+ claimedPaths: persistedClaimed,
909
+ domains: cfg.domains ?? [],
910
+ changeRequestId: result.change_request_id ?? undefined,
911
+ serverSessionId: result.work_session_id,
912
+ });
913
+ if (result.change_request_id) {
914
+ (0, config_1.updateConfig)({ activeChangeRequestId: result.change_request_id });
915
+ }
916
+ process.stdout.write(renderStartupPhase("starting task", "done"));
917
+ await flushStdout();
918
+ process.stdout.write(`[startup] session ${result.created_or_resumed}: ${result.work_session_id}\n`);
919
+ await flushStdout();
920
+ let hadBlockingIssue = false;
921
+ const acceptance = result.acceptance;
922
+ const supervision = (0, supervision_1.supervisionFromAcceptance)(acceptance, cfg.domains ?? []);
923
+ timing.trackSync("render_output", () => {
924
+ process.stdout.write(`${(0, supervision_1.renderSupervisionSummary)(supervision)}\n`);
925
+ });
926
+ const blockingIssue = timing.trackSync("proof_fingerprint_comparison", () => buildWatchBlockingIssue(acceptance, { strictScope: true }));
927
+ if (blockingIssue) {
928
+ timing.trackSync("render_output", () => {
929
+ process.stdout.write(renderWatchBlockingIssue(blockingIssue));
930
+ });
931
+ hadBlockingIssue = true;
932
+ }
933
+ // Surface scope drift from out-of-scope startup dirt unless the acceptance
934
+ // report already raised the same scope-drift issue (avoid double render).
935
+ if (blockingIssue?.errorCode !== "SCOPE_DRIFT_OUT_OF_SCOPE_FILE") {
936
+ const scopeDriftIssue = buildStartupScopeDriftIssue({
937
+ unscopedStartupDirty: classification.unscopedStartupDirty,
938
+ acceptanceCandidateFiles: classification.acceptanceCandidateFiles,
939
+ claimedPaths: persistedClaimed,
940
+ });
941
+ if (scopeDriftIssue) {
942
+ timing.trackSync("render_output", () => {
943
+ process.stdout.write(renderWatchBlockingIssue(scopeDriftIssue));
944
+ });
945
+ hadBlockingIssue = true;
946
+ }
947
+ }
948
+ return { handled: true, hadBlockingIssue };
949
+ }
950
+ async function runWatch(options = {}) {
951
+ const timing = createWatchTimingTracker();
952
+ const cfg = timing.trackSync("config_load", () => (0, config_1.readConfig)());
953
+ const repoRoot = (0, node_process_1.cwd)();
954
+ const gitignoreSessionEntry = (0, gates_1.ensureGitignoreSessionEntry)(repoRoot);
955
+ const ignoredDirtyFiles = new Set();
956
+ if (gitignoreSessionEntry.changed) {
957
+ ignoredDirtyFiles.add(".gitignore");
958
+ }
959
+ const gatesState = (0, gates_1.ensureGatesFile)(repoRoot);
960
+ if (gatesState.malformed) {
961
+ process.stdout.write("[agentbridge] WARNING: .agentbridge/gates.json is unreadable or malformed — treating all gates as warn_only.\n");
962
+ process.stderr.write(`[agentbridge:audit] ${JSON.stringify({
963
+ eventType: "GATES_FILE_PARSE_ERROR",
964
+ message: gatesState.parseError ?? "unknown parse error",
965
+ gates_path: gatesState.path,
966
+ })}\n`);
967
+ }
968
+ const acceptanceRolloutOptions = acceptanceRolloutOptionsFromGates(gatesState.gates);
969
+ const taskScopeInput = parseTaskScopeInput(options);
970
+ const localSession = timing.trackSync("local_session_read", () => (0, session_state_1.readSessionState)());
971
+ const hasActiveLocalSession = isActiveLocalSession(localSession);
972
+ const explicitStrictMode = Boolean(taskScopeInput.task && taskScopeInput.scope);
973
+ if (hasActiveLocalSession && explicitStrictMode) {
974
+ const activeLocalSession = localSession;
975
+ if (!activeLocalSession)
976
+ return;
977
+ const scopeMatch = (activeLocalSession.claimedPaths ?? []).map(normalizeInline).includes(normalizeInline(taskScopeInput.scope));
978
+ const currentTaskSummary = await timing.trackAsync("task_summary_resolution", async () => resolveCurrentSessionTaskSummary(activeLocalSession));
979
+ const taskMatch = currentTaskSummary != null &&
980
+ normalizeTaskSummary(currentTaskSummary) === normalizeTaskSummary(taskScopeInput.task);
981
+ if (!scopeMatch || !taskMatch) {
982
+ process.stdout.write([
983
+ "A different AgentBridge task is already active.",
984
+ "",
985
+ `Current task: ${currentTaskSummary ?? "(unknown)"}`,
986
+ `Current scope: ${(activeLocalSession.claimedPaths ?? []).join(", ") || "(unknown)"}`,
987
+ "",
988
+ "Next action:",
989
+ "- Finish/clear the current task (agentbridge done or agentbridge session abandon --reason \"...\")",
990
+ '- Or explicitly start a new one: agentbridge start --summary "<new task>" --scope "<new scope>"',
991
+ "",
992
+ ].join("\n"));
993
+ process.exitCode = 1;
994
+ return;
995
+ }
996
+ }
997
+ const startupScope = explicitStrictMode
998
+ ? taskScopeInput.scope ?? localSession?.claimedPaths?.[0] ?? "(scope unavailable)"
999
+ : taskScopeInput.scope;
1000
+ const startupTask = explicitStrictMode
1001
+ ? taskScopeInput.task ?? "Current AgentBridge task"
1002
+ : taskScopeInput.task ?? "Inferred from changed files/current work";
1003
+ process.stdout.write(renderWatchStartupHeader({
1004
+ mode: explicitStrictMode ? "explicit" : "inferred",
1005
+ task: startupTask,
1006
+ scope: startupScope,
1007
+ currentStatus: "Starting watch runtime.",
1008
+ nextGuidance: explicitStrictMode
1009
+ ? "AgentBridge will enforce explicit scope and proof checks."
1010
+ : "AgentBridge will classify brainstorming vs coding and infer task/scope from repo activity.",
1011
+ }));
1012
+ await flushStdout();
1013
+ process.stdout.write(renderStartupPhase("resolving project", "starting"));
1014
+ await flushStdout();
1015
+ timing.trackSync("identity_resolution", () => {
1016
+ if (!cfg.activeAgentId) {
1017
+ throw new errors_1.SafeCliError({
1018
+ code: "IDENTITY_NO_ACTIVE_AGENT",
1019
+ category: "IDENTITY_ERROR",
1020
+ what: "No active agent configured.",
1021
+ why: "Watch needs a WorkIdentity selected for this repo.",
1022
+ next: "Run `agentbridge identity list` then `agentbridge use <agent-id>`.",
1023
+ });
1024
+ }
1025
+ if (!cfg.domains || cfg.domains.length === 0) {
1026
+ throw new errors_1.SafeCliError({
1027
+ code: "CONFIG_NO_DOMAIN_MAP",
1028
+ category: "CONFIG_ERROR",
1029
+ what: "No recovered domain map found in .agentbridge/config.json.",
1030
+ why: "Watch uses domain boundaries to enforce scoped file tracking.",
1031
+ next: "Run `agentbridge init` to recover domains for this repo.",
1032
+ });
1033
+ }
1034
+ });
1035
+ let activeAgentId = cfg.activeAgentId;
1036
+ if (options.changeRequestId || options.executionSurfaceId) {
1037
+ (0, config_1.updateConfig)({
1038
+ ...(options.changeRequestId ? { activeChangeRequestId: options.changeRequestId } : {}),
1039
+ ...(options.executionSurfaceId ? { executionSurfaceId: options.executionSurfaceId } : {}),
1040
+ });
1041
+ }
1042
+ if (!options.once) {
1043
+ try {
1044
+ await timing.trackAsync("project_access_check", async () => withTimeout((async () => {
1045
+ const requestedCr = options.changeRequestId ?? cfg.activeChangeRequestId ?? null;
1046
+ const hasLocalServerSession = Boolean((0, session_state_1.readSessionState)()?.serverSessionId);
1047
+ if (requestedCr || hasLocalServerSession) {
1048
+ const resolution = await (0, work_context_resolver_1.resolveWorkContext)((0, config_1.contextFromConfig)(), {
1049
+ changeRequestId: requestedCr,
1050
+ });
1051
+ if (resolution.state === "mismatch") {
1052
+ process.stdout.write(`${(0, work_context_resolver_1.renderWorkContextLines)(resolution).join("\n")}\n`);
1053
+ return;
1054
+ }
1055
+ }
1056
+ })(), WATCH_STARTUP_TIMEOUT_MS, () => new errors_1.SafeCliError({
1057
+ code: "WATCH_STARTUP_TIMEOUT",
1058
+ category: "WATCH_ERROR",
1059
+ what: "Watch startup timed out while resolving project context.",
1060
+ why: "Project/session context could not be resolved in time.",
1061
+ next: "Run `agentbridge doctor`, then retry `agentbridge start`.",
1062
+ suggestedPrompt: "AgentBridge could not start watching this task quickly. Run doctor, confirm project access, then retry.",
1063
+ })));
1064
+ }
1065
+ catch {
1066
+ // If network context is unavailable, continue in local-only mode.
1067
+ }
1068
+ }
1069
+ process.stdout.write(renderStartupPhase("resolving project", "done"));
1070
+ await flushStdout();
1071
+ // Fast path: collapse the one-shot review into a single bounded server call.
1072
+ // Falls through to the legacy multi-call flow when the server predates the
1073
+ // consolidated endpoint or no server context is available. `startingTaskAnnounced`
1074
+ // guards against re-printing the "starting task" phase line in the fallback path.
1075
+ let startingTaskAnnounced = false;
1076
+ const canUseFastPath = acceptanceRolloutOptions.rolloutProofTooWeak === "warn_only" &&
1077
+ acceptanceRolloutOptions.rolloutProofNotRelevant === "warn_only" &&
1078
+ acceptanceRolloutOptions.rolloutImpactCoverageGap === "warn_only";
1079
+ if (options.once && explicitStrictMode && canUseFastPath) {
1080
+ process.stdout.write(renderStartupPhase("starting task", "starting"));
1081
+ await flushStdout();
1082
+ startingTaskAnnounced = true;
1083
+ const fast = await runWatchOnceFastPath({
1084
+ cfg,
1085
+ activeAgentId,
1086
+ task: taskScopeInput.task,
1087
+ scope: taskScopeInput.scope,
1088
+ changeRequestId: hasActiveLocalSession
1089
+ ? options.changeRequestId ?? cfg.activeChangeRequestId ?? null
1090
+ : options.changeRequestId ?? null,
1091
+ allowDirty: Boolean(options.allowDirty) || Boolean(options.daemon),
1092
+ timing,
1093
+ ignoredDirtyFiles,
1094
+ });
1095
+ if (fast.handled) {
1096
+ process.stdout.write("[startup] ready: one-shot run complete.\n");
1097
+ await flushStdout();
1098
+ process.exitCode = fast.hadBlockingIssue ? 1 : 0;
1099
+ timing.flushTotal();
1100
+ return;
1101
+ }
1102
+ }
1103
+ if (options.once && explicitStrictMode && !canUseFastPath) {
1104
+ process.stdout.write("[startup] fast path disabled: rollout gates are active; using full acceptance-check flow.\n");
1105
+ }
1106
+ if (!hasActiveLocalSession && explicitStrictMode) {
1107
+ if (!startingTaskAnnounced) {
1108
+ process.stdout.write(renderStartupPhase("starting task", "starting"));
1109
+ await flushStdout();
1110
+ }
1111
+ try {
1112
+ await timing.trackAsync("start_or_resume", async () => withTimeout((0, start_1.runStart)({
1113
+ summary: taskScopeInput.task,
1114
+ scope: taskScopeInput.scope,
1115
+ }), WATCH_START_TASK_TIMEOUT_MS, () => new errors_1.SafeCliError({
1116
+ code: "WATCH_STARTUP_TIMEOUT",
1117
+ category: "WATCH_ERROR",
1118
+ what: "Watch startup timed out while starting scoped task/session.",
1119
+ why: "Internal start did not complete before timeout.",
1120
+ next: "Run `agentbridge start --summary \"...\" --scope \"...\"` manually and retry watch.",
1121
+ suggestedPrompt: "AgentBridge could not start watching this task quickly. Run doctor, confirm project access, then retry.",
1122
+ })));
1123
+ }
1124
+ catch (startErr) {
1125
+ if (startErr instanceof errors_1.SafeCliError)
1126
+ throw startErr;
1127
+ const reason = startErr instanceof Error ? startErr.message : String(startErr);
1128
+ throw new errors_1.SafeCliError({
1129
+ code: "WATCH_STARTUP_TASK_CREATE_FAILED",
1130
+ category: "WATCH_ERROR",
1131
+ what: "Watch could not create or resume the scoped task/session.",
1132
+ why: reason,
1133
+ next: "Run `agentbridge start --summary \"...\" --scope \"...\"` and retry watch.",
1134
+ });
1135
+ }
1136
+ process.stdout.write(renderStartupPhase("starting task", "done"));
1137
+ }
1138
+ else {
1139
+ process.stdout.write(renderStartupPhase("starting task", "skipped"));
1140
+ }
1141
+ await flushStdout();
1142
+ const startupSession = (0, session_state_1.readSessionState)();
1143
+ if (explicitStrictMode) {
1144
+ if (!startupSession || startupSession.status === "closed" || startupSession.id === "none") {
1145
+ throw new errors_1.SafeCliError({
1146
+ code: "WATCH_STARTUP_SESSION_MISSING",
1147
+ category: "WATCH_ERROR",
1148
+ what: "Watch startup could not find an active local session.",
1149
+ why: "Session creation/resume did not complete.",
1150
+ next: "Run `agentbridge start --summary \"...\" --scope \"...\"` and retry watch.",
1151
+ });
1152
+ }
1153
+ process.stdout.write(`[startup] session created/resumed: ${startupSession.id}\n`);
1154
+ await flushStdout();
1155
+ }
1156
+ else {
1157
+ process.stdout.write("[startup] session: inferred mode (will start tracking when coding begins)\n");
1158
+ await flushStdout();
1159
+ }
1160
+ // In daemon mode, skip interactive dirty-file prompt (treat as --allow-dirty).
1161
+ await timing.trackAsync("repo_clean_check", async () => ensureWatchRepoClean(Boolean(options.allowDirty) || Boolean(options.daemon), ignoredDirtyFiles));
1162
+ process.stdout.write(renderStartupPhase("checking workspace", "done"));
1163
+ await flushStdout();
1164
+ const domainPacketsLoaded = new Set();
1165
+ if (!options.once) {
1166
+ try {
1167
+ const packetCtx = (0, config_1.contextFromConfig)();
1168
+ const projectPacket = await timing.trackAsync("project_packet_fetch", async () => withTimeout((0, server_sync_1.fetchProjectPacket)(packetCtx), WATCH_STARTUP_TIMEOUT_MS, () => new errors_1.SafeCliError({
1169
+ code: "WATCH_STARTUP_TIMEOUT",
1170
+ category: "WATCH_ERROR",
1171
+ what: "Watch startup timed out while loading project packet.",
1172
+ why: "Project packet fetch did not finish promptly.",
1173
+ next: "Run `agentbridge doctor`; watch will continue with packet load skipped.",
1174
+ suggestedPrompt: "AgentBridge could not start watching this task quickly. Run doctor, confirm project access, then retry.",
1175
+ })));
1176
+ if (process.stdout.isTTY) {
1177
+ process.stdout.write(`${(0, watch_packet_handshake_1.renderProjectPacketBanner)(projectPacket, cfg.domains ?? [])}\n\n`);
1178
+ }
1179
+ else {
1180
+ process.stdout.write(`${(0, watch_packet_handshake_1.renderProjectPacketOneLiner)(projectPacket)}\n`);
1181
+ }
1182
+ }
1183
+ catch (err) {
1184
+ const message = err instanceof Error ? err.message : String(err);
1185
+ process.stdout.write(`Packet load skipped: ${message}\n`);
1186
+ process.stdout.write("Domain memory unavailable at watch startup; continuing with empty obligations. Memory-derived proof requirements may be incomplete for this run.\n");
1187
+ }
1188
+ }
1189
+ let idleTimer = null;
1190
+ let handling = Promise.resolve();
1191
+ let lastSupervisionSignature = null;
1192
+ let lastBlockingIssueSignature = null;
1193
+ let lastDriftIssueSignature = null;
1194
+ let hadBlockingIssue = false;
1195
+ const closeIdleSession = async () => {
1196
+ const state = (0, session_state_1.readSessionState)();
1197
+ if (!state || state.status === "closed" || state.id === "none")
1198
+ return;
1199
+ const result = (0, session_1.closeLocalSession)(state);
1200
+ if (!result.ok) {
1201
+ process.stdout.write(`Session remains open: ${result.reason}\n`);
1202
+ return;
1203
+ }
1204
+ if (state.serverSessionId) {
1205
+ try {
1206
+ const ctx = (0, config_1.contextFromConfig)();
1207
+ await (0, server_sync_1.closeServerSession)(ctx, { workSessionId: state.serverSessionId }, {
1208
+ closeOutcome: state.crossings.some((crossing) => crossing.status === "unresolved")
1209
+ ? "blocked"
1210
+ : "completed",
1211
+ closeReason: state.closeReason === "abandoned"
1212
+ ? "abandoned"
1213
+ : state.crossings.some((crossing) => crossing.status === "unresolved")
1214
+ ? "blocked"
1215
+ : "completed",
1216
+ note: "Idle-close from local watch lifecycle.",
1217
+ changedFiles: [...state.changedFiles],
1218
+ domainBreakdown: state.changedFiles.reduce((acc, changed) => {
1219
+ const domain = (0, domain_resolution_1.resolveDomainForFile)(changed, state.domains).domain ?? "unclassified";
1220
+ acc[domain] = (acc[domain] ?? 0) + 1;
1221
+ return acc;
1222
+ }, {}),
1223
+ crossings: state.crossings.map((crossing) => ({
1224
+ file: crossing.file,
1225
+ domain: crossing.domain,
1226
+ status: crossing.status,
1227
+ tier: crossing.tier,
1228
+ })),
1229
+ scopedApprovals: state.approvals.map((approval) => ({
1230
+ domain: approval.domain,
1231
+ file: approval.file,
1232
+ mode: approval.mode,
1233
+ })),
1234
+ implementationHandoff: {
1235
+ feature_or_fix: "Local watcher session close",
1236
+ last_implemented: {
1237
+ summary: "Closed due to idle timeout.",
1238
+ files_changed: state.changedFiles,
1239
+ behavior_added: [],
1240
+ behavior_modified: [],
1241
+ assumptions_made: [],
1242
+ migration_or_schema_changes: [],
1243
+ config_changes: [],
1244
+ },
1245
+ last_tested: {
1246
+ actual: "Test runner executed on idle close.",
1247
+ },
1248
+ next_debug_start: [],
1249
+ },
1250
+ });
1251
+ }
1252
+ catch (err) {
1253
+ process.stdout.write(`${renderIdleCloseFailure(err)}\n`);
1254
+ }
1255
+ }
1256
+ const testRun = await (0, test_runner_1.runDetectedTests)();
1257
+ process.stdout.write(`${(0, briefing_1.renderSessionBriefing)({ state, testRun })}\n`);
1258
+ };
1259
+ const resetIdle = () => {
1260
+ if (idleTimer)
1261
+ clearTimeout(idleTimer);
1262
+ idleTimer = setTimeout(() => {
1263
+ void closeIdleSession();
1264
+ }, IDLE_CLOSE_MS);
1265
+ };
1266
+ const onAbsolutePathChange = async (absolutePath) => {
1267
+ handling = handling.then(async () => {
1268
+ const file = (0, node_path_1.relative)((0, node_process_1.cwd)(), absolutePath).replaceAll("\\", "/");
1269
+ if (!file || file.startsWith(".."))
1270
+ return;
1271
+ // Fix D: Don't reset the idle timer in --once mode. Only accept/abandon/done
1272
+ // should close the session; --once should leave it open for follow-up commands.
1273
+ if (!options.once)
1274
+ resetIdle();
1275
+ let state = (0, session_state_1.readSessionState)();
1276
+ if (!state || state.status === "closed" || state.id === "none") {
1277
+ const lane = (0, domain_resolution_1.inferLaneFromFiles)([file], cfg.domains ?? []);
1278
+ let serverSessionId;
1279
+ const changeRequestId = explicitStrictMode
1280
+ ? options.changeRequestId ?? cfg.activeChangeRequestId
1281
+ : options.changeRequestId ?? state?.changeRequestId;
1282
+ const executionSurfaceId = options.executionSurfaceId ?? cfg.executionSurfaceId;
1283
+ if (changeRequestId && executionSurfaceId) {
1284
+ try {
1285
+ const ctx = (0, config_1.contextFromConfig)();
1286
+ const opened = await openOrResumeServerSessionForWatch({
1287
+ ctx,
1288
+ changeRequestId,
1289
+ executionSurfaceId,
1290
+ file,
1291
+ activeAgentId,
1292
+ inferredLaneDomain: lane.laneDomain,
1293
+ });
1294
+ if (opened) {
1295
+ serverSessionId = opened.workSessionId;
1296
+ if (opened.resumed) {
1297
+ process.stdout.write(`Resumed existing run: ${opened.workSessionId}\n`);
1298
+ }
1299
+ }
1300
+ else {
1301
+ process.stdout.write("Task is already running but no unique active run could be resolved; continuing local-only.\n");
1302
+ }
1303
+ }
1304
+ catch (err) {
1305
+ process.stdout.write(`Server session open failed; continuing local-only: ${String(err)}\n`);
1306
+ }
1307
+ }
1308
+ state = (0, session_1.openLocalSession)({
1309
+ agentId: activeAgentId,
1310
+ laneDomain: lane.laneDomain,
1311
+ claimedPaths: [file],
1312
+ domains: cfg.domains ?? [],
1313
+ serverSessionId,
1314
+ });
1315
+ process.stdout.write([
1316
+ "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
1317
+ " AgentBridge session started",
1318
+ "",
1319
+ ` Identity ${state.agentId}`,
1320
+ ` Allowed lane ${state.laneDomain ?? "unclassified"}`,
1321
+ ` Server sync ${state.serverSessionId ? "enabled" : "local-only"}`,
1322
+ "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
1323
+ "",
1324
+ ].join("\n"));
1325
+ }
1326
+ const outcome = (0, watch_core_1.applyFileChange)(state, file);
1327
+ (0, session_state_1.writeSessionState)(state);
1328
+ const unresolvedProtectedCrossing = state.crossings.some((crossing) => crossing.status === "unresolved" &&
1329
+ (crossing.tier === "tier_a" || crossing.tier === "tier_b"));
1330
+ let ctx = null;
1331
+ if (state.serverSessionId) {
1332
+ try {
1333
+ ctx = (0, config_1.contextFromConfig)();
1334
+ }
1335
+ catch {
1336
+ ctx = null;
1337
+ }
1338
+ }
1339
+ const supervisionResult = await syncAndBuildSupervisionSnapshot({
1340
+ ctx,
1341
+ state,
1342
+ cfgDomains: cfg.domains ?? [],
1343
+ changedFile: file,
1344
+ outcome,
1345
+ changeRequestId: options.changeRequestId ?? cfg.activeChangeRequestId ?? null,
1346
+ unresolvedProtectedCrossing,
1347
+ rollout: acceptanceRolloutOptions,
1348
+ timing,
1349
+ });
1350
+ if (supervisionResult.syncError) {
1351
+ process.stdout.write(`Observed-diff sync failed: ${supervisionResult.syncError}\n`);
1352
+ }
1353
+ const supervision = supervisionResult.supervision;
1354
+ const nextSummary = shouldRenderSupervisionSummary(lastSupervisionSignature, supervision);
1355
+ if (nextSummary.shouldRender) {
1356
+ timing.trackSync("render_output", () => {
1357
+ process.stdout.write(`${(0, supervision_1.renderSupervisionSummary)(supervision)}\n`);
1358
+ });
1359
+ lastSupervisionSignature = nextSummary.nextSignature;
1360
+ }
1361
+ if (supervisionResult.acceptanceReport) {
1362
+ const blockingIssue = timing.trackSync("proof_fingerprint_comparison", () => buildWatchBlockingIssue(supervisionResult.acceptanceReport, {
1363
+ strictScope: explicitStrictMode,
1364
+ }));
1365
+ const issueSignature = blockingIssue ? JSON.stringify(blockingIssue) : null;
1366
+ if (blockingIssue && issueSignature && issueSignature !== lastBlockingIssueSignature) {
1367
+ timing.trackSync("render_output", () => {
1368
+ process.stdout.write(renderWatchBlockingIssue(blockingIssue));
1369
+ });
1370
+ hadBlockingIssue = true;
1371
+ lastBlockingIssueSignature = issueSignature;
1372
+ }
1373
+ else if (!issueSignature) {
1374
+ lastBlockingIssueSignature = null;
1375
+ }
1376
+ }
1377
+ else {
1378
+ const blockingIssue = timing.trackSync("proof_fingerprint_comparison", () => buildFallbackWatchBlockingIssue(supervision));
1379
+ const issueSignature = blockingIssue ? JSON.stringify(blockingIssue) : null;
1380
+ if (blockingIssue && issueSignature && issueSignature !== lastBlockingIssueSignature) {
1381
+ timing.trackSync("render_output", () => {
1382
+ process.stdout.write(renderWatchBlockingIssue(blockingIssue));
1383
+ });
1384
+ hadBlockingIssue = true;
1385
+ lastBlockingIssueSignature = issueSignature;
1386
+ }
1387
+ else if (!issueSignature) {
1388
+ lastBlockingIssueSignature = null;
1389
+ }
1390
+ }
1391
+ if (!explicitStrictMode) {
1392
+ const drift = timing.trackSync("proof_fingerprint_comparison", () => detectInferredDomainDrift(state.changedFiles, cfg.domains ?? []));
1393
+ const driftSignature = drift ? JSON.stringify(drift.issue) : null;
1394
+ if (drift && driftSignature && driftSignature !== lastDriftIssueSignature) {
1395
+ timing.trackSync("render_output", () => {
1396
+ process.stdout.write(renderWatchBlockingIssue(drift.issue));
1397
+ });
1398
+ if (drift.blocking) {
1399
+ hadBlockingIssue = true;
1400
+ }
1401
+ lastDriftIssueSignature = driftSignature;
1402
+ }
1403
+ else if (!driftSignature) {
1404
+ lastDriftIssueSignature = null;
1405
+ }
1406
+ }
1407
+ if (!outcome.requiresAuthorityPrompt)
1408
+ return;
1409
+ if (outcome.requestType === "handshake") {
1410
+ const crossingDomain = outcome.crossingDomain;
1411
+ if (crossingDomain && !domainPacketsLoaded.has(crossingDomain)) {
1412
+ domainPacketsLoaded.add(crossingDomain);
1413
+ try {
1414
+ const packetCtx = (0, config_1.contextFromConfig)();
1415
+ const domainPacket = await (0, server_sync_1.fetchDomainPacket)(packetCtx, {
1416
+ domainName: crossingDomain,
1417
+ claimedPaths: [file],
1418
+ });
1419
+ const entry = domainPacket.domains[0];
1420
+ if (entry) {
1421
+ process.stdout.write(`${(0, watch_packet_handshake_1.renderDomainPacketBanner)(entry)}\n\n`);
1422
+ }
1423
+ }
1424
+ catch (err) {
1425
+ const message = err instanceof Error ? err.message : String(err);
1426
+ process.stdout.write(`Packet load skipped: ${message}\n`);
1427
+ }
1428
+ }
1429
+ process.stdout.write(`${(0, watch_packet_handshake_1.renderHandshakeRefusalBlock)(file, crossingDomain)}\n\n`);
1430
+ (0, watch_packet_handshake_1.applyHandshakeRefusal)(state, file, outcome, (targetFile) => {
1431
+ (0, revert_crossing_1.revertCrossingFile)(targetFile);
1432
+ });
1433
+ (0, session_state_1.writeSessionState)(state);
1434
+ return;
1435
+ }
1436
+ const owner = (0, domain_resolution_1.resolveDomainForFile)(file, cfg.domains ?? []).ownerAgentId ?? `${outcome.crossingDomain} Agent`;
1437
+ process.stdout.write(`${(0, authority_request_1.renderAuthorityRequest)({
1438
+ requestingAgent: state.agentId,
1439
+ currentTask: "Watched local session",
1440
+ authorizedLane: state.laneDomain ?? "unclassified",
1441
+ targetFile: file,
1442
+ targetDomainOwner: owner,
1443
+ severity: outcome.crossingTier,
1444
+ })}\n`);
1445
+ process.stdout.write(`${renderCrossingContext({
1446
+ changeRequestId: options.changeRequestId ?? cfg.activeChangeRequestId ?? null,
1447
+ workSessionId: state.serverSessionId ?? state.id,
1448
+ worker: state.agentId,
1449
+ claimedLaneDomain: state.laneDomain ?? null,
1450
+ touchedDomain: outcome.crossingDomain ?? null,
1451
+ file,
1452
+ tier: outcome.crossingTier,
1453
+ })}\n`);
1454
+ const response = options.daemon
1455
+ ? { decision: "deny" }
1456
+ : await promptDecision();
1457
+ if (options.daemon) {
1458
+ process.stdout.write(`[daemon] Domain crossing denied automatically (no interactive prompt in daemon mode). Use the dashboard to approve.\n`);
1459
+ }
1460
+ try {
1461
+ (0, watch_core_1.applyBoundaryDecision)(state, file, response.decision, {
1462
+ revertFile: (targetFile) => {
1463
+ (0, revert_crossing_1.revertCrossingFile)(targetFile);
1464
+ process.stdout.write(`Denied ${targetFile}; reverted/deleted out-of-lane file.\n`);
1465
+ },
1466
+ revertAll: (files) => {
1467
+ for (const targetFile of files) {
1468
+ (0, revert_crossing_1.revertCrossingFile)(targetFile);
1469
+ }
1470
+ process.stdout.write(`Abandoning session and reverting ${files.length} file(s).\n`);
1471
+ },
1472
+ }, { limitMode: response.limitMode });
1473
+ }
1474
+ catch (err) {
1475
+ process.stdout.write(`Failed to apply ${response.decision} on ${file}: ${err instanceof Error ? err.message : String(err)}\n`);
1476
+ process.stdout.write("Session remains blocked. Retry deny/abandon or resolve crossing manually.\n");
1477
+ state.status = "blocked";
1478
+ state.blockedAt = state.blockedAt ?? new Date().toISOString();
1479
+ (0, session_state_1.writeSessionState)(state);
1480
+ return;
1481
+ }
1482
+ if (state.serverSessionId && outcome.crossingDomain) {
1483
+ try {
1484
+ const ctx = (0, config_1.contextFromConfig)();
1485
+ await (0, server_sync_1.postScopedApproval)(ctx, { workSessionId: state.serverSessionId }, {
1486
+ decision: response.decision,
1487
+ activeAgentId: state.agentId,
1488
+ ownerAgentId: owner,
1489
+ targetDomain: outcome.crossingDomain,
1490
+ approvedFiles: [file],
1491
+ limitMode: response.decision === "limit_scope"
1492
+ ? response.limitMode === "read_only"
1493
+ ? "read_only"
1494
+ : "file_only"
1495
+ : undefined,
1496
+ reason: `Decision from watch prompt: ${response.decision}`,
1497
+ });
1498
+ }
1499
+ catch (err) {
1500
+ process.stdout.write(`Approval sync failed: ${String(err)}\n`);
1501
+ }
1502
+ }
1503
+ if (response.decision === "approve") {
1504
+ process.stdout.write(`${renderScopedApprovalConfirmation(state.agentId, file, outcome.crossingDomain)}\n`);
1505
+ }
1506
+ if (response.decision === "handoff") {
1507
+ state.pendingHandoffToAgent = owner;
1508
+ state.pendingHandoffDomain = outcome.crossingDomain;
1509
+ (0, session_state_1.archiveClosedSession)(state);
1510
+ (0, session_state_1.archiveHandoff)(state);
1511
+ activeAgentId = owner;
1512
+ (0, config_1.updateConfig)({ activeAgentId: owner });
1513
+ process.stdout.write(`Handoff requested. Active agent switched to ${owner} for takeover.\n`);
1514
+ if (state.serverSessionId) {
1515
+ try {
1516
+ const ctx = (0, config_1.contextFromConfig)();
1517
+ await (0, server_sync_1.closeServerSession)(ctx, { workSessionId: state.serverSessionId }, {
1518
+ closeOutcome: "blocked",
1519
+ closeReason: "handoff_required",
1520
+ note: `Handoff required to ${owner}.`,
1521
+ targetAgentId: owner,
1522
+ targetDomain: outcome.crossingDomain ?? null,
1523
+ filesInvolved: [file],
1524
+ changedFiles: [...state.changedFiles],
1525
+ implementationHandoff: {
1526
+ feature_or_fix: "Cross-domain handoff",
1527
+ next_debug_start: [file],
1528
+ next_prompt: `Continue this task as ${owner} in domain ${outcome.crossingDomain ?? "unknown"}.`,
1529
+ },
1530
+ });
1531
+ }
1532
+ catch (err) {
1533
+ process.stdout.write(`Server handoff close failed: ${String(err)}\n`);
1534
+ }
1535
+ }
1536
+ const takeoverSession = (0, session_1.openLocalSession)({
1537
+ agentId: owner,
1538
+ laneDomain: outcome.crossingDomain,
1539
+ claimedPaths: [file],
1540
+ domains: cfg.domains ?? [],
1541
+ });
1542
+ (0, session_state_1.writeSessionState)(takeoverSession);
1543
+ process.stdout.write(`Takeover session started for ${owner} in ${outcome.crossingDomain ?? "unclassified"}.\n`);
1544
+ return;
1545
+ }
1546
+ if (response.decision === "abandon") {
1547
+ (0, session_state_1.archiveClosedSession)(state);
1548
+ }
1549
+ if (response.decision === "abandon" && state.serverSessionId) {
1550
+ try {
1551
+ const ctx = (0, config_1.contextFromConfig)();
1552
+ await (0, server_sync_1.closeServerSession)(ctx, { workSessionId: state.serverSessionId }, {
1553
+ closeOutcome: "blocked",
1554
+ closeReason: "abandoned",
1555
+ note: "Session abandoned by local watcher decision.",
1556
+ changedFiles: [...state.changedFiles],
1557
+ filesInvolved: [...state.changedFiles],
1558
+ crossings: state.crossings.map((crossing) => ({
1559
+ file: crossing.file,
1560
+ domain: crossing.domain,
1561
+ status: crossing.status,
1562
+ tier: crossing.tier,
1563
+ })),
1564
+ scopedApprovals: state.approvals.map((approval) => ({
1565
+ domain: approval.domain,
1566
+ file: approval.file,
1567
+ mode: approval.mode,
1568
+ })),
1569
+ });
1570
+ }
1571
+ catch (err) {
1572
+ process.stdout.write(`Server abandon close failed: ${String(err)}\n`);
1573
+ }
1574
+ }
1575
+ (0, session_state_1.writeSessionState)(state);
1576
+ });
1577
+ await handling;
1578
+ };
1579
+ const processStartupDirty = async () => {
1580
+ const startupDirtyFiles = timing.trackSync("git_snapshot", () => normalizeDirtyWorkingTreeFiles(filterIgnoredDirtyFiles((0, git_status_1.getDirtyWorkingTreeFiles)(), ignoredDirtyFiles)));
1581
+ if (startupDirtyFiles.length === 0) {
1582
+ if (!explicitStrictMode) {
1583
+ process.stdout.write(`${renderBrainstormingStatus()}\n`);
1584
+ }
1585
+ return;
1586
+ }
1587
+ let startupState = (0, session_state_1.readSessionState)();
1588
+ const changeRequestId = explicitStrictMode
1589
+ ? options.changeRequestId ?? cfg.activeChangeRequestId ?? null
1590
+ : options.changeRequestId ?? startupState?.changeRequestId ?? null;
1591
+ let startupClaimedPaths = [];
1592
+ let startupServerSessionId = null;
1593
+ startupClaimedPaths = [...(startupState?.claimedPaths ?? [])];
1594
+ if (startupState?.serverSessionId) {
1595
+ startupServerSessionId = startupState.serverSessionId;
1596
+ }
1597
+ else if (changeRequestId) {
1598
+ try {
1599
+ const ctx = (0, config_1.contextFromConfig)();
1600
+ const resolved = await (0, work_context_resolver_1.findSingleActiveSessionForChangeRequest)(ctx, changeRequestId);
1601
+ if (resolved && !("ambiguous" in resolved)) {
1602
+ startupServerSessionId = resolved.workSessionId;
1603
+ }
1604
+ }
1605
+ catch {
1606
+ startupServerSessionId = null;
1607
+ }
1608
+ }
1609
+ if (startupServerSessionId) {
1610
+ try {
1611
+ const ctx = (0, config_1.contextFromConfig)();
1612
+ const report = await timing.trackAsync("acceptance_check_fetch", async () => (0, server_sync_1.fetchAcceptanceCheck)(ctx, { workSessionId: startupServerSessionId, ...acceptanceRolloutOptions }));
1613
+ startupClaimedPaths = [...new Set([...startupClaimedPaths, ...(report.claimed_paths ?? [])])];
1614
+ }
1615
+ catch {
1616
+ // Keep local claimed paths when server claimed paths are unavailable.
1617
+ }
1618
+ }
1619
+ const startupClassification = timing.trackSync("baseline_classification", () => startupDirtyClassification({
1620
+ dirtyFiles: startupDirtyFiles,
1621
+ localState: startupState,
1622
+ claimedPaths: startupClaimedPaths,
1623
+ domains: cfg.domains ?? [],
1624
+ inferredMode: !explicitStrictMode,
1625
+ }));
1626
+ if (startupClassification.workspaceDirtySnapshot.length === 0 && !explicitStrictMode) {
1627
+ process.stdout.write(renderBrainstormingStatus());
1628
+ }
1629
+ else if (!explicitStrictMode) {
1630
+ const inferredTask = inferTaskFromFiles(startupClassification.workspaceDirtySnapshot, cfg.domains ?? []);
1631
+ process.stdout.write(renderCodingDetection(startupClassification.workspaceDirtySnapshot, inferredTask));
1632
+ }
1633
+ else {
1634
+ process.stdout.write(`Startup dirty snapshot detected ${startupClassification.workspaceDirtySnapshot.length} file(s).\n`);
1635
+ process.stdout.write(`Startup acceptance candidates: ${startupClassification.acceptanceCandidateFiles.length} file(s).\n`);
1636
+ process.stdout.write(`Startup unscoped dirty files: ${startupClassification.unscopedStartupDirty.length} file(s).\n`);
1637
+ if (startupClassification.unscopedStartupDirty.length > 0) {
1638
+ process.stdout.write(`Workspace dirty snapshot (debug only):\n${startupClassification.workspaceDirtySnapshot
1639
+ .map((file) => `- ${file}`)
1640
+ .join("\n")}\n`);
1641
+ process.stdout.write(`Unscoped startup dirty (not posted as observed diff):\n${startupClassification.unscopedStartupDirty
1642
+ .map((file) => `- ${file}`)
1643
+ .join("\n")}\n`);
1644
+ }
1645
+ }
1646
+ if (options.once && explicitStrictMode) {
1647
+ const startupScopeDriftIssue = timing.trackSync("proof_fingerprint_comparison", () => buildStartupScopeDriftIssue({
1648
+ unscopedStartupDirty: startupClassification.unscopedStartupDirty,
1649
+ acceptanceCandidateFiles: startupClassification.acceptanceCandidateFiles,
1650
+ claimedPaths: startupClaimedPaths,
1651
+ }));
1652
+ if (startupScopeDriftIssue) {
1653
+ timing.trackSync("render_output", () => {
1654
+ process.stdout.write(renderWatchBlockingIssue(startupScopeDriftIssue));
1655
+ });
1656
+ hadBlockingIssue = true;
1657
+ }
1658
+ }
1659
+ if (!startupClassification.scopeProven && explicitStrictMode) {
1660
+ process.stdout.write("Startup dirty files detected, but no scoped current work is proven.\nRun with --change-request or claim scope.\n");
1661
+ }
1662
+ else if (startupClassification.acceptanceCandidateFiles.length === 0 && explicitStrictMode) {
1663
+ process.stdout.write("Startup dirty snapshot is outside current scoped work. Waiting for in-scope changes.\n");
1664
+ }
1665
+ if (startupClassification.scopeProven &&
1666
+ startupClassification.acceptanceCandidateFiles.length > 0 &&
1667
+ startupServerSessionId &&
1668
+ (!startupState || startupState.status === "closed" || startupState.id === "none")) {
1669
+ const lane = (0, domain_resolution_1.inferLaneFromFiles)(startupClassification.acceptanceCandidateFiles, cfg.domains ?? []);
1670
+ startupState = (0, session_1.openLocalSession)({
1671
+ agentId: activeAgentId,
1672
+ laneDomain: lane.laneDomain,
1673
+ claimedPaths: startupClaimedPaths.length > 0
1674
+ ? startupClaimedPaths
1675
+ : [...startupClassification.acceptanceCandidateFiles],
1676
+ domains: cfg.domains ?? [],
1677
+ serverSessionId: startupServerSessionId,
1678
+ });
1679
+ (0, session_state_1.writeSessionState)(startupState);
1680
+ process.stdout.write(`Linked local session ${startupState.id} to server session ${startupServerSessionId} before startup supervision.\n`);
1681
+ }
1682
+ for (const file of startupClassification.acceptanceCandidateFiles) {
1683
+ await onAbsolutePathChange((0, node_path_1.resolve)((0, node_process_1.cwd)(), file));
1684
+ }
1685
+ };
1686
+ let stop = null;
1687
+ if (!options.once) {
1688
+ process.stdout.write(renderStartupPhase("starting watcher", "starting"));
1689
+ await flushStdout();
1690
+ stop = timing.trackSync("watcher_startup", () => (0, watcher_1.startWatcher)((0, node_process_1.cwd)(), (absolutePath) => {
1691
+ void onAbsolutePathChange(absolutePath);
1692
+ }));
1693
+ process.stdout.write(renderStartupPhase("starting watcher", "done"));
1694
+ await flushStdout();
1695
+ }
1696
+ else {
1697
+ process.stdout.write(renderStartupPhase("starting watcher", "skipped"));
1698
+ await flushStdout();
1699
+ }
1700
+ await processStartupDirty();
1701
+ process.stdout.write("[startup] ready: watch runtime is live.\n");
1702
+ await flushStdout();
1703
+ if (options.once) {
1704
+ if (idleTimer)
1705
+ clearTimeout(idleTimer);
1706
+ process.stdout.write("[startup] ready: one-shot run complete.\n");
1707
+ process.exitCode = hadBlockingIssue ? 1 : 0;
1708
+ timing.flushTotal();
1709
+ return;
1710
+ }
1711
+ process.on("SIGINT", () => {
1712
+ if (idleTimer)
1713
+ clearTimeout(idleTimer);
1714
+ stop?.();
1715
+ process.stdout.write("\nagentbridge watch stopped.\n");
1716
+ process.exit(0);
1717
+ });
1718
+ }