@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
package/dist/errors.js ADDED
@@ -0,0 +1,276 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SafeCliError = void 0;
4
+ exports.catalogCliError = catalogCliError;
5
+ exports.formatCliError = formatCliError;
6
+ exports.renderCliErrorView = renderCliErrorView;
7
+ exports.exitCodeForCategory = exitCodeForCategory;
8
+ exports.renderCliError = renderCliError;
9
+ const http_1 = require("./http");
10
+ const error_catalog_1 = require("./error-catalog");
11
+ const redact_secrets_1 = require("./redact-secrets");
12
+ class SafeCliError extends Error {
13
+ isSafeCliError = true;
14
+ code;
15
+ category;
16
+ why;
17
+ next;
18
+ suggestedPrompt;
19
+ constructor(input) {
20
+ if (typeof input === "string") {
21
+ super(input);
22
+ return;
23
+ }
24
+ super(input.what);
25
+ this.code = input.code;
26
+ this.category = input.category;
27
+ this.why = input.why;
28
+ this.next = input.next;
29
+ this.suggestedPrompt = input.suggestedPrompt;
30
+ }
31
+ }
32
+ exports.SafeCliError = SafeCliError;
33
+ function defaultErrorView(message) {
34
+ const debugMode = process.env.AGENTBRIDGE_DEBUG_HTTP === "1" || process.env.AGENTBRIDGE_DEBUG === "1";
35
+ const catalog = (0, error_catalog_1.catalogEntryForCode)("UNKNOWN_RUNTIME_ERROR");
36
+ const sanitizedMessage = debugMode
37
+ ? message
38
+ : (catalog?.what ??
39
+ "The command could not be completed due to an unexpected local/runtime error.");
40
+ return {
41
+ title: catalog?.title ?? "Command failed.",
42
+ what: sanitizedMessage,
43
+ why: catalog?.why ??
44
+ "AgentBridge could not complete the request because the failure was not classified.",
45
+ next: catalog?.next ?? "Run `agentbridge doctor` and retry.",
46
+ code: catalog?.code ?? "UNKNOWN_RUNTIME_ERROR",
47
+ category: catalog?.category ?? "UNKNOWN_ERROR",
48
+ };
49
+ }
50
+ function parseErrorCode(body) {
51
+ if (!body)
52
+ return null;
53
+ try {
54
+ const parsed = JSON.parse(body);
55
+ if (typeof parsed.code === "string" && parsed.code.trim().length > 0) {
56
+ return (0, error_catalog_1.mapServerCodeToCatalog)(parsed.code);
57
+ }
58
+ if (typeof parsed.error === "string" && parsed.error.trim().length > 0) {
59
+ return (0, error_catalog_1.mapServerCodeToCatalog)(parsed.error);
60
+ }
61
+ if (parsed.error && typeof parsed.error === "object" && parsed.error !== null) {
62
+ const nested = parsed.error;
63
+ if (typeof nested.code === "string" && nested.code.trim().length > 0) {
64
+ return (0, error_catalog_1.mapServerCodeToCatalog)(nested.code);
65
+ }
66
+ }
67
+ return null;
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }
73
+ function catalogCliError(code, overrides) {
74
+ const entry = (0, error_catalog_1.catalogEntryForCode)(code);
75
+ if (entry) {
76
+ return new SafeCliError({
77
+ code: entry.code,
78
+ category: entry.category,
79
+ what: overrides?.what ?? entry.what,
80
+ why: overrides?.why ?? entry.why,
81
+ next: overrides?.next ?? entry.next,
82
+ });
83
+ }
84
+ return new SafeCliError({
85
+ code,
86
+ what: overrides?.what ?? code,
87
+ why: overrides?.why ?? "AgentBridge could not complete the request.",
88
+ next: overrides?.next ?? "Run `agentbridge doctor` and retry.",
89
+ });
90
+ }
91
+ function viewFromCatalogOrFallback(code, fallback) {
92
+ const mapped = code ? (0, error_catalog_1.mapServerCodeToCatalog)(code) : null;
93
+ const entry = (0, error_catalog_1.catalogEntryForCode)(mapped ?? undefined);
94
+ if (!entry)
95
+ return fallback;
96
+ return {
97
+ title: entry.title,
98
+ what: entry.what,
99
+ why: entry.why,
100
+ next: entry.next,
101
+ code: entry.code,
102
+ category: entry.category,
103
+ };
104
+ }
105
+ function formatCliError(error) {
106
+ if (error instanceof SafeCliError) {
107
+ const entry = error.code ? (0, error_catalog_1.catalogEntryForCode)(error.code) : null;
108
+ return {
109
+ title: entry?.title ?? "Command failed.",
110
+ what: error.message,
111
+ why: error.why ??
112
+ entry?.why ??
113
+ "AgentBridge could not complete the request.",
114
+ next: error.next ??
115
+ entry?.next ??
116
+ "Follow the next actions listed above.",
117
+ code: error.code ?? entry?.code,
118
+ category: error.category ?? entry?.category,
119
+ suggestedPrompt: error.suggestedPrompt,
120
+ };
121
+ }
122
+ if (error instanceof http_1.CliMissingConfigError) {
123
+ const entry = (0, error_catalog_1.catalogEntryForCode)(error.code);
124
+ return {
125
+ title: entry?.title ?? "Command failed.",
126
+ what: entry?.what ?? error.message,
127
+ why: entry?.why ??
128
+ "AgentBridge cannot call the server without project credentials.",
129
+ next: entry?.next ?? "Run `agentbridge init` or set AGENTBRIDGE_API_KEY.",
130
+ code: error.code,
131
+ category: entry?.category ?? "CONFIG_ERROR",
132
+ };
133
+ }
134
+ if (error instanceof http_1.CliHttpError) {
135
+ const code = parseErrorCode(error.body);
136
+ if (error.status === 404) {
137
+ return viewFromCatalogOrFallback(code, {
138
+ title: "No active current session.",
139
+ what: "AgentBridge could not find the requested or current work session.",
140
+ why: "The local session pointer and server state are out of sync.",
141
+ next: "Run `agentbridge session list --active`.",
142
+ code: code ?? "WORK_CONTEXT_NO_CURRENT_SESSION",
143
+ category: "WORK_CONTEXT_ERROR",
144
+ });
145
+ }
146
+ if (error.status === 409) {
147
+ return viewFromCatalogOrFallback(code, {
148
+ title: "Request conflicted with current state.",
149
+ what: "AgentBridge rejected this action because the resource state changed or conflicts with policy.",
150
+ why: "Some state transitions are only allowed from specific lifecycle states.",
151
+ next: "Refresh context with `agentbridge status` and retry the appropriate command.",
152
+ code: code ?? "ACCEPTANCE_BLOCKED",
153
+ category: "ACCEPTANCE_ERROR",
154
+ });
155
+ }
156
+ if (error.status === 401 || error.status === 403) {
157
+ return viewFromCatalogOrFallback(code, {
158
+ title: "Authentication failed.",
159
+ what: "The API key was missing, invalid, or lacks project access.",
160
+ why: "AgentBridge cannot read or mutate project work state without valid credentials.",
161
+ next: "Confirm `.agentbridge/config.json` and rerun `agentbridge init` if needed.",
162
+ code: code ?? "PROJECT_ACCESS_UNAUTHORIZED",
163
+ category: "AUTH_ERROR",
164
+ });
165
+ }
166
+ if (error.status === 422) {
167
+ return viewFromCatalogOrFallback(code, {
168
+ title: "Request validation failed.",
169
+ what: "The command payload failed server-side validation.",
170
+ why: "Required fields or lifecycle preconditions were not satisfied.",
171
+ next: "Review command flags and rerun, or run `agentbridge doctor` for config checks.",
172
+ code: code ?? "CONFIG_INCOMPLETE",
173
+ category: "CONFIG_ERROR",
174
+ });
175
+ }
176
+ if (error.status >= 500) {
177
+ return viewFromCatalogOrFallback(code, {
178
+ title: "Server error.",
179
+ what: "The AgentBridge server failed while handling the request.",
180
+ why: "The action could not complete until the server recovers.",
181
+ next: "Retry shortly. If it persists, run `agentbridge doctor`.",
182
+ code: code ?? "SERVER_ERROR",
183
+ category: "SERVER_ERROR",
184
+ });
185
+ }
186
+ return viewFromCatalogOrFallback(code, {
187
+ title: `HTTP ${error.status} from AgentBridge.`,
188
+ what: "The server rejected or failed the request.",
189
+ why: "The server rejected or failed the request.",
190
+ next: "Run `agentbridge doctor` and retry.",
191
+ code: code ?? `HTTP_${error.status}`,
192
+ category: "SERVER_ERROR",
193
+ });
194
+ }
195
+ // Network errors: ECONNREFUSED, ETIMEDOUT, ENOTFOUND, ECONNRESET
196
+ if (error instanceof Error) {
197
+ const nodeCode = error.code;
198
+ if (nodeCode === "ECONNREFUSED" ||
199
+ nodeCode === "ETIMEDOUT" ||
200
+ nodeCode === "ENOTFOUND" ||
201
+ nodeCode === "ECONNRESET") {
202
+ return {
203
+ title: "Network error.",
204
+ what: "Cannot reach AgentBridge server. Is it running on the configured host?",
205
+ why: "The CLI could not establish a TCP connection to the server.",
206
+ next: "Check that the server is running and AGENTBRIDGE_BASE_URL is correct. Run `agentbridge doctor`.",
207
+ code: "NETWORK_UNREACHABLE",
208
+ category: "NETWORK_ERROR",
209
+ };
210
+ }
211
+ return defaultErrorView(error.message);
212
+ }
213
+ return defaultErrorView(String(error));
214
+ }
215
+ function renderCliErrorView(view) {
216
+ const whatHappened = (0, redact_secrets_1.redactSecrets)(`${view.title} ${view.what}`.trim());
217
+ const lines = [
218
+ `What happened: ${whatHappened}`,
219
+ `Why it matters: ${(0, redact_secrets_1.redactSecrets)(view.why)}`,
220
+ `Next action: ${(0, redact_secrets_1.redactSecrets)(view.next)}`,
221
+ ];
222
+ if (view.suggestedPrompt) {
223
+ lines.push("Suggested prompt to send back to agent:");
224
+ lines.push("```text");
225
+ lines.push((0, redact_secrets_1.redactSecrets)(view.suggestedPrompt));
226
+ lines.push("```");
227
+ }
228
+ if (view.code) {
229
+ lines.push(`Error code: ${view.code}`);
230
+ }
231
+ return lines.join("\n");
232
+ }
233
+ /**
234
+ * Standard consolidated exit codes:
235
+ * 0 — success
236
+ * 1 — blocking issue (proof, scope drift, work-context, acceptance, handoff)
237
+ * 2 — infrastructure error (network, server 5xx, unknown runtime)
238
+ * 3 — user input error (bad args, config, auth, identity, project access, memory)
239
+ */
240
+ function exitCodeForCategory(category) {
241
+ switch (category) {
242
+ case "WORK_CONTEXT_ERROR":
243
+ case "PROOF_ERROR":
244
+ case "PROOF_STALE_ERROR":
245
+ case "SCOPE_DRIFT_ERROR":
246
+ case "ACCEPTANCE_ERROR":
247
+ case "HANDOFF_ERROR":
248
+ return 1;
249
+ case "CONFIG_ERROR":
250
+ case "AUTH_ERROR":
251
+ case "PROJECT_ACCESS_ERROR":
252
+ case "IDENTITY_ERROR":
253
+ case "MEMORY_ERROR":
254
+ return 3;
255
+ case "NETWORK_ERROR":
256
+ case "SERVER_ERROR":
257
+ case "START_ERROR":
258
+ case "WATCH_ERROR":
259
+ case "UNKNOWN_ERROR":
260
+ default:
261
+ return 2;
262
+ }
263
+ }
264
+ function renderCliError(error) {
265
+ if (error instanceof SafeCliError) {
266
+ const view = formatCliError(error);
267
+ if (error.message.includes("What happened:")) {
268
+ if (view.code && !error.message.includes("Error code:")) {
269
+ return `${error.message}\nError code: ${view.code}`;
270
+ }
271
+ return error.message;
272
+ }
273
+ return renderCliErrorView(view);
274
+ }
275
+ return renderCliErrorView(formatCliError(error));
276
+ }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.computeFileFingerprints = computeFileFingerprints;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const node_fs_1 = require("node:fs");
6
+ const MAX_HASH_BYTES = 5 * 1024 * 1024;
7
+ function normalizePath(path) {
8
+ return path.trim().replaceAll("\\", "/");
9
+ }
10
+ function sha256File(path) {
11
+ const stat = (0, node_fs_1.statSync)(path);
12
+ if (!stat.isFile())
13
+ return undefined;
14
+ if (stat.size > MAX_HASH_BYTES)
15
+ return undefined;
16
+ const content = (0, node_fs_1.readFileSync)(path);
17
+ return (0, node_crypto_1.createHash)("sha256").update(content).digest("hex");
18
+ }
19
+ function computeFileFingerprints(paths) {
20
+ const uniquePaths = [...new Set(paths.map(normalizePath).filter((path) => path.length > 0))].sort();
21
+ return uniquePaths.map((path) => {
22
+ try {
23
+ const stat = (0, node_fs_1.statSync)(path);
24
+ const fileType = stat.isFile()
25
+ ? "file"
26
+ : stat.isDirectory()
27
+ ? "directory"
28
+ : "other";
29
+ return {
30
+ path,
31
+ exists: true,
32
+ fileType,
33
+ size: stat.size,
34
+ mtimeMs: stat.mtimeMs,
35
+ sha256: fileType === "file" ? sha256File(path) : undefined,
36
+ };
37
+ }
38
+ catch {
39
+ return {
40
+ path,
41
+ exists: false,
42
+ };
43
+ }
44
+ });
45
+ }
package/dist/gates.js ADDED
@@ -0,0 +1,200 @@
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.GITIGNORE_SESSION_LINE = exports.STATS_HISTORY_CAP = exports.DEFAULT_GATES = void 0;
7
+ exports.computeFpRate = computeFpRate;
8
+ exports.checkAdvancementEligibility = checkAdvancementEligibility;
9
+ exports.applyRolloutGate = applyRolloutGate;
10
+ exports.parseGatesFile = parseGatesFile;
11
+ exports.readGatesFile = readGatesFile;
12
+ exports.updateGateStats = updateGateStats;
13
+ exports.appendStatsHistory = appendStatsHistory;
14
+ exports.atomicWriteGatesJson = atomicWriteGatesJson;
15
+ exports.mergeWriteGatesJson = mergeWriteGatesJson;
16
+ exports.cleanupStaleTempFiles = cleanupStaleTempFiles;
17
+ exports.ensureGitignoreSessionEntry = ensureGitignoreSessionEntry;
18
+ exports.ensureGatesFile = ensureGatesFile;
19
+ const node_fs_1 = __importDefault(require("node:fs"));
20
+ const node_path_1 = __importDefault(require("node:path"));
21
+ exports.DEFAULT_GATES = {
22
+ rollout_proof_too_weak: "warn_only",
23
+ rollout_proof_not_relevant: "warn_only",
24
+ rollout_impact_coverage_gap: "warn_only",
25
+ };
26
+ exports.STATS_HISTORY_CAP = 20;
27
+ exports.GITIGNORE_SESSION_LINE = ".agentbridge/session.json";
28
+ function computeFpRate(stats) {
29
+ if (stats.firings === 0)
30
+ return 0;
31
+ if (stats.fp_rate >= 0 && Number.isFinite(stats.fp_rate))
32
+ return stats.fp_rate;
33
+ return stats.likely_fp / stats.firings;
34
+ }
35
+ function checkAdvancementEligibility(stats, opts) {
36
+ const minFirings = opts?.minFirings ?? 10;
37
+ const maxFpRate = opts?.maxFpRate ?? 0.1;
38
+ if (stats.firings < minFirings) {
39
+ return { eligible: false, reason: `firings ${stats.firings} < ${minFirings}` };
40
+ }
41
+ const fpRate = computeFpRate(stats);
42
+ if (fpRate > maxFpRate) {
43
+ return { eligible: false, reason: `fp rate ${fpRate.toFixed(3)} > ${maxFpRate}` };
44
+ }
45
+ return { eligible: true };
46
+ }
47
+ function applyRolloutGate(input) {
48
+ const hasBlockers = input.blockingCodes.length > 0;
49
+ if (!hasBlockers) {
50
+ return { effectiveDecision: "accepted", warnings: [], blocked: false };
51
+ }
52
+ if (input.stage === "warn_only") {
53
+ return {
54
+ effectiveDecision: "accepted",
55
+ warnings: input.blockingCodes.map((c) => `[warn_only] ${c}`),
56
+ blocked: false,
57
+ };
58
+ }
59
+ if (input.stage === "soft_fail") {
60
+ if (input.overrideUsed) {
61
+ return {
62
+ effectiveDecision: "needs_proof",
63
+ warnings: input.blockingCodes.map((c) => `[soft_fail override] ${c}`),
64
+ blocked: false,
65
+ };
66
+ }
67
+ return {
68
+ effectiveDecision: "blocked",
69
+ warnings: input.blockingCodes.map((c) => `[soft_fail] ${c}`),
70
+ blocked: true,
71
+ };
72
+ }
73
+ return {
74
+ effectiveDecision: "blocked",
75
+ warnings: input.blockingCodes.map((c) => `[hard_fail] ${c}`),
76
+ blocked: true,
77
+ };
78
+ }
79
+ function parseGatesFile(raw) {
80
+ try {
81
+ const parsed = JSON.parse(raw);
82
+ return {
83
+ gates: {
84
+ ...exports.DEFAULT_GATES,
85
+ ...parsed,
86
+ rollout_impact_coverage_gap: parsed.rollout_impact_coverage_gap ?? parsed.impact_coverage ?? exports.DEFAULT_GATES.rollout_impact_coverage_gap,
87
+ rollout_proof_too_weak: parsed.rollout_proof_too_weak ?? parsed.obligation_set ?? exports.DEFAULT_GATES.rollout_proof_too_weak,
88
+ rollout_proof_not_relevant: parsed.rollout_proof_not_relevant ?? parsed.obligation_set ?? exports.DEFAULT_GATES.rollout_proof_not_relevant,
89
+ },
90
+ };
91
+ }
92
+ catch (err) {
93
+ return {
94
+ gates: { ...exports.DEFAULT_GATES },
95
+ parseError: err instanceof Error ? err.message : String(err),
96
+ };
97
+ }
98
+ }
99
+ function readGatesFile(filePath) {
100
+ if (!node_fs_1.default.existsSync(filePath)) {
101
+ return { gates: { ...exports.DEFAULT_GATES }, malformed: false };
102
+ }
103
+ const raw = node_fs_1.default.readFileSync(filePath, "utf8");
104
+ const { gates, parseError } = parseGatesFile(raw);
105
+ return { gates, parseError, malformed: Boolean(parseError) };
106
+ }
107
+ function updateGateStats(stats, event, nowIso) {
108
+ const base = stats ?? {
109
+ firings: 0,
110
+ overrides: 0,
111
+ likely_fp: 0,
112
+ likely_tp: 0,
113
+ fp_rate: 0,
114
+ window_started_at: nowIso,
115
+ };
116
+ const nextFirings = base.firings + (event.fired ? 1 : 0);
117
+ const nextFp = base.likely_fp + (event.falsePositive ? 1 : 0);
118
+ const nextTp = base.likely_tp + (event.fired && !event.falsePositive ? 1 : 0);
119
+ return {
120
+ ...base,
121
+ firings: nextFirings,
122
+ overrides: base.overrides + (event.override ? 1 : 0),
123
+ likely_fp: nextFp,
124
+ likely_tp: nextTp,
125
+ fp_rate: nextFirings === 0 ? 0 : nextFp / nextFirings,
126
+ };
127
+ }
128
+ function appendStatsHistory(history, outgoing, archivedAt) {
129
+ const next = [...(history ?? []), { ...outgoing, archived_at: archivedAt }];
130
+ if (next.length <= exports.STATS_HISTORY_CAP)
131
+ return next;
132
+ return next.slice(next.length - exports.STATS_HISTORY_CAP);
133
+ }
134
+ function atomicWriteGatesJson(filePath, gates) {
135
+ const dir = node_path_1.default.dirname(filePath);
136
+ node_fs_1.default.mkdirSync(dir, { recursive: true });
137
+ const tmp = node_path_1.default.join(dir, `gates.json.tmp.${process.pid}.${Date.now()}`);
138
+ node_fs_1.default.writeFileSync(tmp, `${JSON.stringify(gates, null, 2)}\n`, "utf8");
139
+ node_fs_1.default.renameSync(tmp, filePath);
140
+ }
141
+ function mergeWriteGatesJson(filePath, patch) {
142
+ const { gates: current } = readGatesFile(filePath);
143
+ const merged = { ...current, ...patch };
144
+ atomicWriteGatesJson(filePath, merged);
145
+ return merged;
146
+ }
147
+ function cleanupStaleTempFiles(dir, staleThresholdMs = 60_000) {
148
+ if (!node_fs_1.default.existsSync(dir))
149
+ return [];
150
+ const removed = [];
151
+ const now = Date.now();
152
+ for (const name of node_fs_1.default.readdirSync(dir)) {
153
+ if (!name.startsWith("gates.json.tmp."))
154
+ continue;
155
+ const full = node_path_1.default.join(dir, name);
156
+ const age = now - node_fs_1.default.statSync(full).mtimeMs;
157
+ if (age > staleThresholdMs) {
158
+ node_fs_1.default.unlinkSync(full);
159
+ removed.push(full);
160
+ }
161
+ }
162
+ return removed;
163
+ }
164
+ function ensureGitignoreSessionEntry(repoRoot) {
165
+ const gitignorePath = node_path_1.default.join(repoRoot, ".gitignore");
166
+ const existing = node_fs_1.default.existsSync(gitignorePath)
167
+ ? node_fs_1.default.readFileSync(gitignorePath, "utf8")
168
+ : "";
169
+ const lines = existing.split("\n");
170
+ const hasLine = lines.some((line) => line.trim() === exports.GITIGNORE_SESSION_LINE);
171
+ if (hasLine) {
172
+ return { changed: false, content: existing };
173
+ }
174
+ const suffix = existing.endsWith("\n") || existing.length === 0 ? "" : "\n";
175
+ const next = `${existing}${suffix}${exports.GITIGNORE_SESSION_LINE}\n`;
176
+ node_fs_1.default.writeFileSync(gitignorePath, next, "utf8");
177
+ return { changed: true, content: next };
178
+ }
179
+ function ensureGatesFile(repoRoot) {
180
+ const gatesPath = node_path_1.default.join(repoRoot, ".agentbridge", "gates.json");
181
+ const dir = node_path_1.default.dirname(gatesPath);
182
+ node_fs_1.default.mkdirSync(dir, { recursive: true });
183
+ if (!node_fs_1.default.existsSync(gatesPath)) {
184
+ atomicWriteGatesJson(gatesPath, { ...exports.DEFAULT_GATES });
185
+ return {
186
+ path: gatesPath,
187
+ created: true,
188
+ gates: { ...exports.DEFAULT_GATES },
189
+ malformed: false,
190
+ };
191
+ }
192
+ const parsed = readGatesFile(gatesPath);
193
+ return {
194
+ path: gatesPath,
195
+ created: false,
196
+ gates: parsed.gates,
197
+ malformed: parsed.malformed,
198
+ parseError: parsed.parseError,
199
+ };
200
+ }