@primitive.ai/prim 0.1.0-alpha.91 → 0.1.0-alpha.93

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 (40) hide show
  1. package/README.md +34 -2
  2. package/dist/{chunk-67VNLLBN.js → chunk-2GP5UFV7.js} +1 -0
  3. package/dist/{chunk-VW3FDYRT.js → chunk-36TWVNBE.js} +6 -6
  4. package/dist/{chunk-HU4PW6I6.js → chunk-C3VAA5CG.js} +1 -1
  5. package/dist/{chunk-FOLKHXLJ.js → chunk-DCJN6LZH.js} +37 -5
  6. package/dist/{chunk-M6RIJVRE.js → chunk-DSWT6BCC.js} +78 -4
  7. package/dist/{chunk-FBAYCS3X.js → chunk-DXJQR4QI.js} +150 -38
  8. package/dist/{chunk-PCVGBE5B.js → chunk-G2VMA2QO.js} +2 -1
  9. package/dist/{chunk-NAHHFSLH.js → chunk-ICM2XCXS.js} +2 -2
  10. package/dist/{chunk-OAAP2MY6.js → chunk-K7D2EYBS.js} +2 -2
  11. package/dist/{chunk-5AFRMOPX.js → chunk-OH3EZXYX.js} +1 -1
  12. package/dist/{chunk-KFXF2V5G.js → chunk-OTHKG76J.js} +1 -1
  13. package/dist/chunk-OYHYCK4K.js +13 -0
  14. package/dist/{chunk-FKXFUUWJ.js → chunk-TYAHC25F.js} +1 -1
  15. package/dist/{chunk-5GYQ4J3D.js → chunk-UKUAAAXE.js} +2 -2
  16. package/dist/{chunk-EWZMDD44.js → chunk-X64HTHUF.js} +22 -3
  17. package/dist/{chunk-WX52U7Z2.js → chunk-ZU563PQI.js} +58 -2
  18. package/dist/daemon/server.js +382 -124
  19. package/dist/hook-runtime/daemon/server.js +382 -124
  20. package/dist/hook-runtime/hooks/post-commit.js +57 -39
  21. package/dist/hook-runtime/hooks/post-rewrite.js +58 -40
  22. package/dist/hook-runtime/hooks/post-tool-use.js +111 -78
  23. package/dist/hook-runtime/hooks/pre-tool-use.js +48 -26
  24. package/dist/hook-runtime/hooks/prim-hook.js +157 -117
  25. package/dist/hook-runtime/hooks/session-end.js +1 -1
  26. package/dist/hook-runtime/hooks/session-start.js +38 -4
  27. package/dist/hook-runtime/index.js +1293 -526
  28. package/dist/hook-runtime/statusline-main.js +23 -1
  29. package/dist/hooks/post-commit.js +11 -8
  30. package/dist/hooks/post-rewrite.js +11 -8
  31. package/dist/hooks/post-tool-use.js +13 -15
  32. package/dist/hooks/pre-commit.js +4 -4
  33. package/dist/hooks/pre-tool-use.js +10 -12
  34. package/dist/hooks/prim-hook.js +17 -14
  35. package/dist/hooks/session-end.js +4 -4
  36. package/dist/hooks/session-start.js +12 -36
  37. package/dist/index.js +867 -291
  38. package/dist/statusline-main.js +23 -1
  39. package/package.json +1 -1
  40. package/dist/chunk-BTNNEH2W.js +0 -79
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  ensurePostRewriteHookAtPath,
9
9
  inspectEffectivePostCommitHook,
10
10
  inspectEffectivePostRewriteHook,
11
+ kickDaemonEnsure,
11
12
  postCommitHookBlock,
12
13
  postRewriteHookBlock,
13
14
  printJson,
@@ -17,42 +18,53 @@ import {
17
18
  uninstallPostRewriteHookAtPath,
18
19
  uninstallProjectPostCommitHook,
19
20
  uninstallProjectPostRewriteHook
20
- } from "./chunk-FOLKHXLJ.js";
21
+ } from "./chunk-DCJN6LZH.js";
21
22
  import {
22
23
  requireDurableIngestAcknowledgement
23
- } from "./chunk-FKXFUUWJ.js";
24
+ } from "./chunk-TYAHC25F.js";
24
25
  import {
25
26
  checkAffectedDecisions,
26
27
  daemonOrDirectGet,
27
28
  formatDecisionsWarning
28
- } from "./chunk-5GYQ4J3D.js";
29
+ } from "./chunk-UKUAAAXE.js";
29
30
  import {
30
31
  fetchFeedbackCapability
31
- } from "./chunk-5AFRMOPX.js";
32
+ } from "./chunk-OH3EZXYX.js";
33
+ import {
34
+ isUnattended,
35
+ unattendedEnv
36
+ } from "./chunk-OYHYCK4K.js";
32
37
  import {
33
38
  JOURNAL_DIR,
34
39
  SESSIONS_DIR,
35
40
  bucketStats,
41
+ drainResumeOffset,
42
+ envSlug,
36
43
  listBuckets,
37
44
  listFlushing,
38
45
  pendingJournalStats,
39
- readMovesFromPath
40
- } from "./chunk-FBAYCS3X.js";
46
+ readMovesFromPath,
47
+ removeDrainCheckpoint,
48
+ rotationIdentity,
49
+ sweepOrphanedDrainProgress,
50
+ writeDrainCheckpoint
51
+ } from "./chunk-DXJQR4QI.js";
41
52
  import {
53
+ MS_PER_SECOND,
54
+ deliveryBacklogState,
55
+ deliveryBacklogSummary,
56
+ formatPendingBacklog,
42
57
  formatStatusline,
43
58
  formatTeammates,
44
59
  repositoryBindingDiagnosticLabel
45
- } from "./chunk-WX52U7Z2.js";
60
+ } from "./chunk-ZU563PQI.js";
46
61
  import {
47
62
  bold,
48
63
  boundedHealthError,
49
64
  color,
50
65
  colorForArea,
51
- dim,
52
- stripAnsi
53
- } from "./chunk-BTNNEH2W.js";
54
- import {
55
66
  decisionIngestionStatus,
67
+ dim,
56
68
  fetchAndCacheCollectScope,
57
69
  isCliErrorResponse,
58
70
  isDecisionIdRequest,
@@ -68,8 +80,9 @@ import {
68
80
  isValidRepoSyncId,
69
81
  repoSyncId,
70
82
  repositoryBindingState,
71
- setRepoActive
72
- } from "./chunk-M6RIJVRE.js";
83
+ setRepoActive,
84
+ stripAnsi
85
+ } from "./chunk-DSWT6BCC.js";
73
86
  import {
74
87
  CliManagementResponseError,
75
88
  HttpError,
@@ -79,13 +92,14 @@ import {
79
92
  getPinnedClient,
80
93
  getPinnedManagementClient,
81
94
  getSiteUrl,
95
+ getSiteUrlForEnvironment,
82
96
  getTokenExpiresAt,
83
97
  isSessionEnded,
84
98
  refreshToken,
85
99
  setStoredToken,
86
100
  stripControlChars,
87
101
  terminalSafeLine
88
- } from "./chunk-PCVGBE5B.js";
102
+ } from "./chunk-G2VMA2QO.js";
89
103
  import {
90
104
  assertOwnedHookRuntime,
91
105
  atomicWrite,
@@ -100,6 +114,7 @@ import {
100
114
  inspectHookRuntimeResolutions,
101
115
  inspectHookRuntimeResolutions2,
102
116
  makeRegistration,
117
+ packageRoot,
103
118
  packageVersion,
104
119
  performStatus,
105
120
  performStatus2,
@@ -111,9 +126,10 @@ import {
111
126
  registerCursorCommands,
112
127
  removeHookRuntime,
113
128
  stableHookCommand,
129
+ stableNodePath,
114
130
  stageHookRuntime,
115
131
  stripCommand
116
- } from "./chunk-EWZMDD44.js";
132
+ } from "./chunk-X64HTHUF.js";
117
133
  import {
118
134
  canonicalGitRoot,
119
135
  canonicalRepositoryPath,
@@ -122,7 +138,7 @@ import {
122
138
  import {
123
139
  daemonIsLive,
124
140
  daemonRequest
125
- } from "./chunk-KFXF2V5G.js";
141
+ } from "./chunk-OTHKG76J.js";
126
142
  import {
127
143
  REFRESH_TOKEN_PATH,
128
144
  TOKEN_FILE_PATH,
@@ -135,12 +151,13 @@ import {
135
151
  requestWorkosDeviceAuthorization,
136
152
  resolveAuthCredential,
137
153
  syncDirectory,
138
- withFileLock
139
- } from "./chunk-67VNLLBN.js";
154
+ withFileLock,
155
+ withFileLockSync
156
+ } from "./chunk-2GP5UFV7.js";
140
157
 
141
158
  // src/index.ts
142
- import { readFileSync as readFileSync9 } from "fs";
143
- import { dirname as dirname6, resolve as resolve4 } from "path";
159
+ import { readFileSync as readFileSync10 } from "fs";
160
+ import { dirname as dirname7, resolve as resolve4 } from "path";
144
161
  import { fileURLToPath as fileURLToPath2 } from "url";
145
162
  import { Command } from "commander";
146
163
  import updateNotifier from "update-notifier";
@@ -749,18 +766,18 @@ function parseAuthBrokerConfig(value) {
749
766
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
750
767
  throw new Error("broker config must be an object");
751
768
  }
752
- const record2 = value;
753
- if (typeof record2.authorization_server !== "string" || !record2.authorization_server.trim()) {
769
+ const record3 = value;
770
+ if (typeof record3.authorization_server !== "string" || !record3.authorization_server.trim()) {
754
771
  throw new Error("authorization_server is required");
755
772
  }
756
- if (typeof record2.client_id !== "string" || !record2.client_id.trim()) {
773
+ if (typeof record3.client_id !== "string" || !record3.client_id.trim()) {
757
774
  throw new Error("client_id is required");
758
775
  }
759
- if (!Array.isArray(record2.default_scopes) || !record2.default_scopes.every((scope) => typeof scope === "string" && scope.length > 0)) {
776
+ if (!Array.isArray(record3.default_scopes) || !record3.default_scopes.every((scope) => typeof scope === "string" && scope.length > 0)) {
760
777
  throw new Error("default_scopes must be a string array");
761
778
  }
762
- const authorizationServer = parseHttpUrl(record2.authorization_server, "authorization_server");
763
- const rawEndpoint = record2.authorization_endpoint === void 0 ? DEFAULT_WORKOS_AUTHORIZE_URL : record2.authorization_endpoint;
779
+ const authorizationServer = parseHttpUrl(record3.authorization_server, "authorization_server");
780
+ const rawEndpoint = record3.authorization_endpoint === void 0 ? DEFAULT_WORKOS_AUTHORIZE_URL : record3.authorization_endpoint;
764
781
  if (typeof rawEndpoint !== "string" || !rawEndpoint.trim()) {
765
782
  throw new Error("authorization_endpoint must be a URL");
766
783
  }
@@ -771,8 +788,8 @@ function parseAuthBrokerConfig(value) {
771
788
  return {
772
789
  authorization_server: authorizationServer.toString(),
773
790
  authorization_endpoint: authorizationEndpoint.toString(),
774
- client_id: record2.client_id.trim(),
775
- default_scopes: [...record2.default_scopes]
791
+ client_id: record3.client_id.trim(),
792
+ default_scopes: [...record3.default_scopes]
776
793
  };
777
794
  }
778
795
  async function fetchAuthBrokerConfig(siteUrl, fetchImpl = fetch) {
@@ -2177,7 +2194,7 @@ ${line("project", result.project)}`);
2177
2194
 
2178
2195
  // src/commands/daemon.ts
2179
2196
  import { spawn as spawn2 } from "child_process";
2180
- import { closeSync, existsSync as existsSync4, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync4, unlinkSync as unlinkSync2 } from "fs";
2197
+ import { closeSync as closeSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, openSync as openSync2, readFileSync as readFileSync4, unlinkSync as unlinkSync2 } from "fs";
2181
2198
  import { join as join5 } from "path";
2182
2199
  import { Option as Option2 } from "commander";
2183
2200
 
@@ -2250,12 +2267,12 @@ async function runLatestDaemonBootstrap(ensureCurrent, options = {}) {
2250
2267
  import { spawnSync } from "child_process";
2251
2268
  import { createHash as createHash2, randomBytes as randomBytes3 } from "crypto";
2252
2269
  import {
2253
- constants,
2270
+ constants as constants2,
2254
2271
  accessSync,
2255
2272
  chmodSync,
2256
2273
  copyFileSync,
2257
2274
  existsSync as existsSync3,
2258
- lstatSync as lstatSync2,
2275
+ lstatSync as lstatSync3,
2259
2276
  mkdirSync as mkdirSync2,
2260
2277
  mkdtempSync,
2261
2278
  readFileSync as readFileSync3,
@@ -2264,16 +2281,53 @@ import {
2264
2281
  realpathSync,
2265
2282
  renameSync,
2266
2283
  rmSync,
2284
+ statSync,
2267
2285
  symlinkSync,
2268
2286
  writeFileSync as writeFileSync2
2269
2287
  } from "fs";
2270
2288
  import { homedir as homedir3 } from "os";
2271
2289
  import { basename, dirname as dirname2, isAbsolute, join as join4, resolve as resolve2 } from "path";
2272
2290
 
2291
+ // src/lib/bounded-file.ts
2292
+ import { constants, closeSync, fstatSync, lstatSync as lstatSync2, openSync, readSync } from "fs";
2293
+ function readBoundedRegularFile(path, maxBytes) {
2294
+ let fd;
2295
+ try {
2296
+ const linked = lstatSync2(path);
2297
+ if (!linked.isFile() || linked.size > maxBytes) return void 0;
2298
+ fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
2299
+ const opened = fstatSync(fd);
2300
+ if (!opened.isFile() || opened.dev !== linked.dev || opened.ino !== linked.ino || opened.size > maxBytes) {
2301
+ return void 0;
2302
+ }
2303
+ const buffer = Buffer.allocUnsafe(opened.size + 1);
2304
+ let length = 0;
2305
+ while (length < buffer.length) {
2306
+ const count = readSync(fd, buffer, length, buffer.length - length, length);
2307
+ if (count === 0) break;
2308
+ length += count;
2309
+ }
2310
+ if (length > opened.size) return void 0;
2311
+ return { text: buffer.subarray(0, length).toString("utf8"), ownerUid: opened.uid };
2312
+ } catch {
2313
+ return void 0;
2314
+ } finally {
2315
+ if (fd !== void 0) {
2316
+ try {
2317
+ closeSync(fd);
2318
+ } catch {
2319
+ }
2320
+ }
2321
+ }
2322
+ }
2323
+
2273
2324
  // src/daemon/env-binding.ts
2274
2325
  function normalizeApiUrl(url) {
2275
2326
  return url.trim().replace(/\/+$/, "");
2276
2327
  }
2328
+ function apiUrlsMatch(a, b) {
2329
+ return normalizeApiUrl(a) === normalizeApiUrl(b);
2330
+ }
2277
2331
 
2278
2332
  // src/daemon/launchd.ts
2279
2333
  var LAUNCHD_LABEL = "ai.getprimitive.prim-daemon";
@@ -2292,6 +2346,11 @@ var SERVICE_NOT_FOUND = 113;
2292
2346
  var TRANSITION_IN_PROGRESS = 5;
2293
2347
  var OUTPUT_LIMIT = 4096;
2294
2348
  var DAEMON_DISABLED_CONTENT = "disabled by `prim daemon stop`\n";
2349
+ var LAUNCHER_MAX_BYTES = 64 * 1024;
2350
+ var PLIST_MAX_BYTES = 64 * 1024;
2351
+ var DRIFT_HEAL_MARKER = "daemon-drift-heal.json";
2352
+ var DRIFT_HEAL_MARKER_TEMPORARY_RE = /^daemon-drift-heal\.json\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.tmp$/u;
2353
+ var DRIFT_HEAL_CLAIM_OWNER = "owner.json";
2295
2354
  var LaunchctlError = class extends Error {
2296
2355
  status;
2297
2356
  timedOut;
@@ -2331,6 +2390,8 @@ function daemonControlPaths(options = {}) {
2331
2390
  return {
2332
2391
  launcher: join4(configDir, `prim-daemon-launcher-v${LAUNCHER_SCHEMA_VERSION}`),
2333
2392
  disabledMarker: join4(configDir, "daemon.disabled"),
2393
+ driftHealMarker: join4(configDir, DRIFT_HEAL_MARKER),
2394
+ driftHealClaim: join4(configDir, "daemon-drift-heal.lock"),
2334
2395
  lifecycleLockDir: join4(configDir, "daemon.lifecycle.lock"),
2335
2396
  legacyDisabledMarker: legacy.disabledMarker,
2336
2397
  legacyLifecycleLockDir: legacy.lifecycleLockDir
@@ -2391,7 +2452,7 @@ function readOwnedRuntimeManifest(releaseDir) {
2391
2452
  }
2392
2453
  const manifest = parsed;
2393
2454
  if (manifest.schemaVersion !== RUNTIME_SCHEMA_VERSION || compareSemver(manifest.version, manifest.version) !== 0 || !isAbsolute(manifest.nodePath) || /[\r\n]/u.test(manifest.nodePath) || manifest.daemonFile !== "prim-daemon-server" || !/^[0-9a-f]{64}$/u.test(manifest.daemonSha256) || new Date(manifest.stagedAt).toISOString() !== manifest.stagedAt || raw !== `${JSON.stringify(manifest, null, 2)}
2394
- ` || !lstatSync2(daemonPath).isFile() || sha256File(daemonPath) !== manifest.daemonSha256) {
2455
+ ` || !lstatSync3(daemonPath).isFile() || sha256File(daemonPath) !== manifest.daemonSha256) {
2395
2456
  return null;
2396
2457
  }
2397
2458
  const entries = readdirSync2(releaseDir).sort();
@@ -2400,8 +2461,17 @@ function readOwnedRuntimeManifest(releaseDir) {
2400
2461
  return null;
2401
2462
  }
2402
2463
  }
2464
+ function sameNode(recorded, desired) {
2465
+ if (recorded === desired) return true;
2466
+ try {
2467
+ const target = realpathSync(desired);
2468
+ return recorded !== target && realpathSync(recorded) === target;
2469
+ } catch {
2470
+ return false;
2471
+ }
2472
+ }
2403
2473
  function sameRuntime(current, desired, daemonPath) {
2404
- if (!current || current.version !== desired.version || current.nodePath !== desired.nodePath || current.daemonSha256 !== desired.daemonSha256) {
2474
+ if (!current || current.version !== desired.version || !sameNode(current.nodePath, desired.nodePath) || current.daemonSha256 !== desired.daemonSha256) {
2405
2475
  return false;
2406
2476
  }
2407
2477
  try {
@@ -2429,7 +2499,7 @@ function stageRuntime(options = {}) {
2429
2499
  if (!daemonSource || !existsSync3(daemonSource)) {
2430
2500
  throw new Error("cannot stage runtime: prim-daemon-server bundle is unavailable");
2431
2501
  }
2432
- const nodePath = resolve2(options.nodePath ?? process.execPath);
2502
+ const nodePath = resolve2(options.nodePath ?? stableNodePath());
2433
2503
  const version = options.version ?? findPackageVersion(daemonSource);
2434
2504
  const desired = {
2435
2505
  schemaVersion: RUNTIME_SCHEMA_VERSION,
@@ -2528,13 +2598,30 @@ function assertRegularFile(path, label) {
2528
2598
  }
2529
2599
  function lstatIfPresent2(path) {
2530
2600
  try {
2531
- return lstatSync2(path);
2601
+ return lstatSync3(path);
2532
2602
  } catch (error) {
2533
2603
  const code = error.code;
2534
2604
  if (code === "ENOENT" || code === "ENOTDIR") return void 0;
2535
2605
  throw error;
2536
2606
  }
2537
2607
  }
2608
+ function driftHealMarkerTemporaries(configDir) {
2609
+ try {
2610
+ return readdirSync2(configDir).filter((entry) => DRIFT_HEAL_MARKER_TEMPORARY_RE.test(entry)).map((entry) => join4(configDir, entry));
2611
+ } catch (error) {
2612
+ const code = error.code;
2613
+ if (code === "ENOENT" || code === "ENOTDIR") return [];
2614
+ throw error;
2615
+ }
2616
+ }
2617
+ function assertDriftHealClaim(path) {
2618
+ const claim = lstatIfPresent2(path);
2619
+ if (!claim) return;
2620
+ if (!claim.isDirectory() || readdirSync2(path).some((entry) => entry !== DRIFT_HEAL_CLAIM_OWNER)) {
2621
+ throw new Error(`refusing to remove unrecognized daemon drift-heal claim at ${path}`);
2622
+ }
2623
+ assertRegularFile(join4(path, DRIFT_HEAL_CLAIM_OWNER), "daemon drift-heal claim owner");
2624
+ }
2538
2625
  function assertExactFile(path, expected, label) {
2539
2626
  if (!lstatIfPresent2(path)) return;
2540
2627
  assertRegularFile(path, label);
@@ -2573,17 +2660,16 @@ function assertOwnedDaemonRuntime(options) {
2573
2660
  );
2574
2661
  }
2575
2662
  assertRegularFile(logPath, "daemon log");
2663
+ assertRegularFile(control.driftHealMarker, "daemon drift-heal marker");
2664
+ for (const temporary of driftHealMarkerTemporaries(configDir)) {
2665
+ assertRegularFile(temporary, "daemon drift-heal marker temporary");
2666
+ }
2667
+ assertDriftHealClaim(control.driftHealClaim);
2576
2668
  assertRegularFile(join4(configDir, "daemon-health.json"), "daemon health snapshot");
2577
2669
  assertRegularFile(join4(configDir, "client_instance_id"), "daemon client instance id");
2578
2670
  if (service && lstatIfPresent2(service.plistPath)) {
2579
2671
  assertRegularFile(service.plistPath, "launchd property list");
2580
- const expected = generateLaunchAgentPlist({
2581
- launcherPath: control.launcher,
2582
- logPath,
2583
- workingDirectory: options.homeDir ?? homedir3(),
2584
- label: options.label
2585
- });
2586
- if (readFileSync3(service.plistPath, "utf8") !== expected) {
2672
+ if (readFileSync3(service.plistPath, "utf8") !== ownedLaunchAgentPlist(options, logPath)) {
2587
2673
  throw new Error(
2588
2674
  `refusing to remove unrecognized launchd property list at ${service.plistPath}`
2589
2675
  );
@@ -2660,6 +2746,9 @@ async function removeDaemonRuntime(options = {}) {
2660
2746
  control.launcher,
2661
2747
  control.disabledMarker,
2662
2748
  control.legacyDisabledMarker,
2749
+ control.driftHealMarker,
2750
+ ...driftHealMarkerTemporaries(configDir),
2751
+ control.driftHealClaim,
2663
2752
  ...service ? [service.plistPath] : [],
2664
2753
  logPath,
2665
2754
  join4(configDir, "daemon-health.json"),
@@ -2671,7 +2760,7 @@ async function removeDaemonRuntime(options = {}) {
2671
2760
  return withDaemonLifecycleLock(async () => {
2672
2761
  assertOwnedDaemonRuntime(options);
2673
2762
  for (const path of ownedArtifacts.filter((candidate) => candidate !== runtime.runtimeDir)) {
2674
- rmSync(path, { force: true });
2763
+ rmSync(path, { force: true, recursive: path === control.driftHealClaim });
2675
2764
  }
2676
2765
  if (lstatIfPresent2(runtime.runtimeDir)) {
2677
2766
  const quarantined = `${runtime.runtimeDir}.uninstall-${String(process.pid)}-${randomBytes3(8).toString("hex")}`;
@@ -2705,9 +2794,22 @@ ${config.configDir ? `export PRIM_CONFIG_DIR=${shellQuote(config.configDir)}
2705
2794
  `
2706
2795
  };
2707
2796
  }
2797
+ function ownedLaunchAgentPlist(options, logPath) {
2798
+ return generateLaunchAgentPlist({
2799
+ launcherPath: daemonControlPaths(options).launcher,
2800
+ logPath,
2801
+ workingDirectory: options.homeDir ?? homedir3(),
2802
+ label: options.label
2803
+ });
2804
+ }
2708
2805
  function readDaemonLauncher(path) {
2806
+ return readDaemonLauncherFile(path)?.config ?? null;
2807
+ }
2808
+ function readDaemonLauncherFile(path) {
2709
2809
  try {
2710
- const content = readFileSync3(path, "utf8");
2810
+ const file = readBoundedRegularFile(path, LAUNCHER_MAX_BYTES);
2811
+ if (!file) return null;
2812
+ const content = file.text;
2711
2813
  const encoded = /^# prim-daemon-launcher: ([A-Za-z0-9_-]+)$/mu.exec(content)?.[1];
2712
2814
  if (!encoded) return null;
2713
2815
  const value = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
@@ -2722,7 +2824,7 @@ function readDaemonLauncher(path) {
2722
2824
  ...typeof value.configDir === "string" ? { configDir: value.configDir } : {}
2723
2825
  };
2724
2826
  const expected = generateDaemonLauncher(config);
2725
- return expected.revision === value.revision && expected.content === content ? config : null;
2827
+ return expected.revision === value.revision && expected.content === content ? { config, ownerUid: file.ownerUid } : null;
2726
2828
  } catch {
2727
2829
  return null;
2728
2830
  }
@@ -2732,7 +2834,7 @@ function readUsableDaemonConfig(path) {
2732
2834
  if (!config) return null;
2733
2835
  const manifest = readRuntimeManifest(join4(dirname2(config.daemonPath), "manifest.json"));
2734
2836
  try {
2735
- accessSync(config.nodePath, constants.X_OK);
2837
+ accessSync(config.nodePath, constants2.X_OK);
2736
2838
  return manifest?.version === config.runtimeVersion && manifest.nodePath === config.nodePath && manifest.daemonSha256 === sha256File(config.daemonPath) ? config : null;
2737
2839
  } catch {
2738
2840
  return null;
@@ -3076,6 +3178,68 @@ function setDaemonExplicitlyDisabled(disabled, options = {}) {
3076
3178
  }
3077
3179
  atomicWrite2(paths.disabledMarker, content, RUNTIME_FILE_MODE);
3078
3180
  }
3181
+ function launcherRunnable(config) {
3182
+ try {
3183
+ accessSync(config.nodePath, constants2.X_OK);
3184
+ return statSync(config.daemonPath).isFile();
3185
+ } catch {
3186
+ return false;
3187
+ }
3188
+ }
3189
+ function selectedDaemonLauncher(options = {}) {
3190
+ const file = readDaemonLauncherFile(daemonControlPaths(options).launcher);
3191
+ if (!file) return null;
3192
+ const { config, ownerUid } = file;
3193
+ return {
3194
+ runtimeVersion: config.runtimeVersion,
3195
+ ...config.apiUrl ? { apiUrl: config.apiUrl } : {},
3196
+ ownerUid,
3197
+ runnable: launcherRunnable(config)
3198
+ };
3199
+ }
3200
+ var XML_ENTITIES = {
3201
+ "&amp;": "&",
3202
+ "&lt;": "<",
3203
+ "&gt;": ">",
3204
+ "&quot;": '"',
3205
+ "&apos;": "'"
3206
+ };
3207
+ var XML_ENTITY_RE = /&(?:amp|lt|gt|quot|apos);/gu;
3208
+ var PLIST_LABEL_KEY = "<key>Label</key>";
3209
+ var PLIST_PROGRAM_KEY = "<key>ProgramArguments</key>";
3210
+ var PLIST_LABEL_RE = /<key>Label<\/key>\s*<string>([^<]*)<\/string>/u;
3211
+ var PLIST_PROGRAM_RE = /<key>ProgramArguments<\/key>\s*<array>\s*<string>([^<]*)<\/string>/u;
3212
+ function xmlUnescape(value) {
3213
+ if (value.replace(XML_ENTITY_RE, "").includes("&")) return void 0;
3214
+ return value.replace(XML_ENTITY_RE, (entity) => XML_ENTITIES[entity] ?? entity);
3215
+ }
3216
+ function launchAgentProgram(plist) {
3217
+ if (plist.split(PLIST_LABEL_KEY).length !== 2 || plist.split(PLIST_PROGRAM_KEY).length !== 2) {
3218
+ return void 0;
3219
+ }
3220
+ const label = PLIST_LABEL_RE.exec(plist)?.[1];
3221
+ const program2 = PLIST_PROGRAM_RE.exec(plist)?.[1];
3222
+ if (label === void 0 || program2 === void 0) return void 0;
3223
+ const decodedLabel = xmlUnescape(label);
3224
+ const decodedProgram = xmlUnescape(program2);
3225
+ return decodedLabel === void 0 || decodedProgram === void 0 ? void 0 : { label: decodedLabel, program: decodedProgram };
3226
+ }
3227
+ function launchAgentRunsConfigRoot(options = {}) {
3228
+ try {
3229
+ const service = launchdPaths(options);
3230
+ const plist = readBoundedRegularFile(service.plistPath, PLIST_MAX_BYTES);
3231
+ const agent = plist ? launchAgentProgram(plist.text) : void 0;
3232
+ return agent?.label === (options.label ?? LAUNCHD_LABEL) && agent.program === daemonControlPaths(options).launcher;
3233
+ } catch {
3234
+ return false;
3235
+ }
3236
+ }
3237
+ function daemonDriftHealMarkerPath(options = {}) {
3238
+ return daemonControlPaths(options).driftHealMarker;
3239
+ }
3240
+ function daemonDriftHealClaimPath(options = {}) {
3241
+ return daemonControlPaths(options).driftHealClaim;
3242
+ }
3079
3243
  async function ensureMacDaemon(options = {}) {
3080
3244
  return withDaemonLifecycleLock(() => ensureMacDaemonLocked(options), options);
3081
3245
  }
@@ -3277,16 +3441,20 @@ var STATUS_PROBE_TIMEOUT_MS = 500;
3277
3441
  var READY_TIMEOUT_MS = 5e3;
3278
3442
  var READY_POLL_MS2 = 100;
3279
3443
  var READY_PROBE_TIMEOUT_MS2 = 250;
3280
- var HEALTHY_TIMEOUT_MS = 3e4;
3281
- var HEALTHY_POLL_MS = 250;
3444
+ var READY_SNAPSHOT_TIMEOUT_MS = 3e4;
3445
+ var READY_SNAPSHOT_POLL_MS = 250;
3282
3446
  var EXIT_OK3 = 0;
3283
3447
  var EXIT_NOT_RUNNING = 2;
3284
3448
  var EXIT_BOOTING = 3;
3285
- function formatDaemonLifecycleMessage(message, decisionIngestion) {
3286
- return `${message} \xB7 Decision ingestion ${decisionIngestion}`;
3449
+ var VERIFIED_PREFIX = "[prim] \u2713 ";
3450
+ function formatDaemonLifecycleMessage(message, decisionIngestion, snapshot, now = Date.now()) {
3451
+ const backlog = deliveryBacklogSummary(snapshot?.ingestion, now);
3452
+ const state = deliveryBacklogState(snapshot?.ingestion);
3453
+ const verdict = (state === "failing" || state === "retained") && message.startsWith(VERIFIED_PREFIX) ? `[prim] \u26A0 ${message.slice(VERIFIED_PREFIX.length)}` : message;
3454
+ return `${verdict} \xB7 Decision ingestion ${decisionIngestion}${backlog ? ` \xB7 ${backlog}` : ""}`;
3287
3455
  }
3288
- function formatCurrentDaemonLifecycleMessage(message) {
3289
- return formatDaemonLifecycleMessage(message, decisionIngestionStatus(process.cwd()));
3456
+ function formatCurrentDaemonLifecycleMessage(message, snapshot) {
3457
+ return formatDaemonLifecycleMessage(message, decisionIngestionStatus(process.cwd()), snapshot);
3290
3458
  }
3291
3459
  function daemonDegradedReason(snapshot) {
3292
3460
  if (!snapshot || snapshot.healthy !== false) return void 0;
@@ -3337,7 +3505,7 @@ function openDaemonLog(configDir = CONFIG_DIR) {
3337
3505
  if (!existsSync4(configDir)) {
3338
3506
  mkdirSync3(configDir, { recursive: true, mode: CONFIG_DIR_MODE });
3339
3507
  }
3340
- return openSync(join5(configDir, "daemon.log"), "a", LOG_FILE_MODE);
3508
+ return openSync2(join5(configDir, "daemon.log"), "a", LOG_FILE_MODE);
3341
3509
  }
3342
3510
  async function waitForReady() {
3343
3511
  const deadline = Date.now() + READY_TIMEOUT_MS;
@@ -3349,17 +3517,23 @@ async function waitForReady() {
3349
3517
  }
3350
3518
  return daemonIsLive(READY_PROBE_TIMEOUT_MS2);
3351
3519
  }
3352
- async function waitForHealthySnapshot(expectedVersion) {
3353
- const deadline = Date.now() + HEALTHY_TIMEOUT_MS;
3520
+ function requestStatusSnapshot() {
3521
+ return daemonRequest(
3522
+ "status_snapshot",
3523
+ {},
3524
+ { timeoutMs: STATUS_PROBE_TIMEOUT_MS }
3525
+ );
3526
+ }
3527
+ async function waitForReadySnapshot(expectedVersion, options = {}) {
3528
+ const request = options.requestSnapshot ?? requestStatusSnapshot;
3529
+ const wait = options.sleep ?? sleep;
3530
+ const nowMs = options.nowMs ?? Date.now;
3531
+ const deadline = nowMs() + READY_SNAPSHOT_TIMEOUT_MS;
3354
3532
  let snapshot = null;
3355
- while (Date.now() < deadline) {
3356
- snapshot = await daemonRequest(
3357
- "status_snapshot",
3358
- {},
3359
- { timeoutMs: STATUS_PROBE_TIMEOUT_MS }
3360
- );
3361
- if (snapshot?.healthy === true && snapshot.version === expectedVersion) return snapshot;
3362
- await sleep(HEALTHY_POLL_MS);
3533
+ while (nowMs() < deadline) {
3534
+ snapshot = await request();
3535
+ if (daemonStartIsReady(true, snapshot, expectedVersion)) return snapshot;
3536
+ await wait(READY_SNAPSHOT_POLL_MS);
3363
3537
  }
3364
3538
  return snapshot;
3365
3539
  }
@@ -3372,11 +3546,20 @@ async function verifiedPid(existing) {
3372
3546
  );
3373
3547
  return snapshot?.pid === existing.pid ? snapshot : null;
3374
3548
  }
3375
- function daemonStartIsHealthy(serviceReady, snapshot, expectedVersion) {
3376
- return serviceReady && snapshot?.healthy === true && expectedVersion !== void 0 && snapshot.version === expectedVersion;
3549
+ function daemonStartIsReady(serviceReady, snapshot, expectedVersion) {
3550
+ if (!serviceReady || !snapshot || expectedVersion === void 0) return false;
3551
+ if (snapshot.version !== expectedVersion || snapshot.needsReauth === true) return false;
3552
+ if (snapshot.healthy === true) return true;
3553
+ return snapshot.heartbeat?.healthy === true && snapshot.ingestion?.healthy === false;
3554
+ }
3555
+ function daemonBacklogFields(snapshot) {
3556
+ const state = deliveryBacklogState(snapshot?.ingestion);
3557
+ if (!state) return {};
3558
+ if (state === "draining") return { draining: true, ingestion: snapshot?.ingestion };
3559
+ return state === "retained" ? { draining: false, deliveryRetained: true, ingestion: snapshot?.ingestion } : { draining: false, deliveryFailing: true, ingestion: snapshot?.ingestion };
3377
3560
  }
3378
- function daemonStartHealthFields(healthy, snapshot) {
3379
- if (healthy) return {};
3561
+ function daemonStartHealthFields(ready, snapshot) {
3562
+ if (ready) return daemonBacklogFields(snapshot);
3380
3563
  return {
3381
3564
  state: "degraded",
3382
3565
  needsReauth: snapshot?.needsReauth === true,
@@ -3384,16 +3567,28 @@ function daemonStartHealthFields(healthy, snapshot) {
3384
3567
  ingestion: snapshot?.ingestion ?? null
3385
3568
  };
3386
3569
  }
3570
+ function daemonStartFailureReason(snapshot, expectedVersion) {
3571
+ if (snapshot && !snapshot.needsReauth && expectedVersion !== void 0 && snapshot.version !== expectedVersion) {
3572
+ return `daemon reports ${snapshot.version ? `v${snapshot.version}` : "no version"}, expected v${expectedVersion}`;
3573
+ }
3574
+ return daemonDegradedReason(snapshot);
3575
+ }
3387
3576
  async function detachedDaemonStart(opts) {
3388
3577
  const existing = readPidfile();
3389
3578
  if (existing?.alive) {
3390
3579
  const snapshot = await verifiedPid(existing);
3391
3580
  if (snapshot) {
3392
3581
  process.stderr.write(
3393
- `${formatCurrentDaemonLifecycleMessage(`[prim] daemon already running (pid=${existing.pid})`)}
3582
+ `${formatCurrentDaemonLifecycleMessage(`[prim] daemon already running (pid=${existing.pid})`, snapshot)}
3394
3583
  `
3395
3584
  );
3396
- console.log(JSON.stringify({ started: false, pid: existing.pid }, null, 2));
3585
+ console.log(
3586
+ JSON.stringify(
3587
+ { started: false, pid: existing.pid, ...daemonBacklogFields(snapshot) },
3588
+ null,
3589
+ 2
3590
+ )
3591
+ );
3397
3592
  return;
3398
3593
  }
3399
3594
  process.stderr.write(
@@ -3442,16 +3637,19 @@ async function detachedDaemonStart(opts) {
3442
3637
  });
3443
3638
  child.unref();
3444
3639
  if (logFd !== void 0) {
3445
- closeSync(logFd);
3640
+ closeSync2(logFd);
3446
3641
  }
3447
3642
  const live = await waitForReady();
3448
3643
  if (live) {
3449
3644
  const after = readPidfile();
3645
+ const snapshot = await requestStatusSnapshot();
3450
3646
  process.stderr.write(
3451
- `${formatCurrentDaemonLifecycleMessage(`[prim] \u2713 daemon started (pid=${after?.pid ?? "?"}, socket=${SOCK_PATH})`)}
3647
+ `${formatCurrentDaemonLifecycleMessage(`[prim] \u2713 daemon started (pid=${after?.pid ?? "?"}, socket=${SOCK_PATH})`, snapshot)}
3452
3648
  `
3453
3649
  );
3454
- console.log(JSON.stringify({ started: true, pid: after?.pid }, null, 2));
3650
+ console.log(
3651
+ JSON.stringify({ started: true, pid: after?.pid, ...daemonBacklogFields(snapshot) }, null, 2)
3652
+ );
3455
3653
  return;
3456
3654
  }
3457
3655
  process.stderr.write(
@@ -3574,19 +3772,25 @@ async function macDaemonStart(forceRestart = false) {
3574
3772
  const result = await ensureMacDaemon({ explicitlyStarted: true, forceRestart });
3575
3773
  const serviceReady = result.state === "running";
3576
3774
  const expectedVersion = result.runtime?.manifest.version;
3577
- const snapshot = serviceReady && expectedVersion ? await waitForHealthySnapshot(expectedVersion) : null;
3578
- const healthy = daemonStartIsHealthy(serviceReady, snapshot, expectedVersion);
3579
- if (healthy) {
3775
+ const snapshot = serviceReady && expectedVersion ? await waitForReadySnapshot(expectedVersion) : null;
3776
+ const ready = daemonStartIsReady(serviceReady, snapshot, expectedVersion);
3777
+ if (ready) {
3580
3778
  const verb = result.action === "none" ? "already running" : "started";
3581
3779
  const servicePid = result.service.loaded ? result.service.pid : void 0;
3582
3780
  process.stderr.write(
3583
- `${formatCurrentDaemonLifecycleMessage(`[prim] \u2713 daemon ${verb} under launchd (pid=${snapshot?.pid ?? servicePid ?? "?"})`)}
3781
+ `${formatCurrentDaemonLifecycleMessage(`[prim] \u2713 daemon ${verb} under launchd (pid=${snapshot?.pid ?? servicePid ?? "?"})`, snapshot)}
3584
3782
  `
3585
3783
  );
3784
+ } else if (!(serviceReady && expectedVersion)) {
3785
+ process.stderr.write(
3786
+ `[prim] \u2717 launchd did not converge on the desired daemon (${result.action}) (see ${LOG_PATH})
3787
+ `
3788
+ );
3789
+ if (!process.exitCode) process.exitCode = EXIT_NOT_RUNNING;
3586
3790
  } else {
3587
- const reason = daemonDegradedReason(snapshot);
3791
+ const reason = daemonStartFailureReason(snapshot, expectedVersion);
3588
3792
  process.stderr.write(
3589
- `[prim] \u2717 launchd daemon did not reach healthy heartbeat + ingestion state${reason ? ` \xB7 ${reason}` : ""} (see ${LOG_PATH})
3793
+ `[prim] \u2717 launchd daemon did not reach a healthy heartbeat on the expected version${reason ? ` \xB7 ${reason}` : ""} (see ${LOG_PATH})
3590
3794
  `
3591
3795
  );
3592
3796
  if (!process.exitCode) process.exitCode = EXIT_NOT_RUNNING;
@@ -3594,14 +3798,20 @@ async function macDaemonStart(forceRestart = false) {
3594
3798
  console.log(
3595
3799
  JSON.stringify(
3596
3800
  {
3597
- started: healthy,
3801
+ started: ready,
3598
3802
  supervised: true,
3599
3803
  action: result.action,
3600
3804
  pid: snapshot?.pid ?? (result.service.loaded ? result.service.pid : void 0),
3601
3805
  loaded: result.service.loaded,
3602
3806
  responding: result.responding,
3603
- healthy,
3604
- ...daemonStartHealthFields(healthy, snapshot),
3807
+ // `healthy` keeps meaning "start succeeded" (always equal to `started`
3808
+ // and the exit code) so existing consumers stay correct. It is not the
3809
+ // daemon's own health: `daemon status --json` stays degraded while a
3810
+ // backlog drains, is held back, or fails to deliver, which start
3811
+ // reports additively via `draining`, `deliveryRetained`,
3812
+ // `deliveryFailing`, and `ingestion`.
3813
+ healthy: ready,
3814
+ ...daemonStartHealthFields(ready, snapshot),
3605
3815
  version: snapshot?.version,
3606
3816
  expectedVersion
3607
3817
  },
@@ -3905,11 +4115,12 @@ async function daemonEnsure() {
3905
4115
  return { disabled };
3906
4116
  }
3907
4117
  const result = await ensureMacDaemon();
4118
+ const snapshot = result.state === "running" ? await requestStatusSnapshot() : null;
3908
4119
  if (result.state === "disabled") {
3909
4120
  process.stderr.write("[prim] daemon remains explicitly disabled\n");
3910
4121
  } else if (result.state === "running") {
3911
4122
  process.stderr.write(
3912
- `${formatCurrentDaemonLifecycleMessage(`[prim] \u2713 daemon ensured under launchd (${result.action})`)}
4123
+ `${formatCurrentDaemonLifecycleMessage(`[prim] \u2713 daemon ensured under launchd (${result.action})`, snapshot)}
3913
4124
  `
3914
4125
  );
3915
4126
  } else {
@@ -3923,7 +4134,8 @@ async function daemonEnsure() {
3923
4134
  ensured: result.state === "running",
3924
4135
  disabled: result.state === "disabled",
3925
4136
  supervised: true,
3926
- action: result.action
4137
+ action: result.action,
4138
+ ...daemonBacklogFields(snapshot)
3927
4139
  },
3928
4140
  null,
3929
4141
  2
@@ -4958,11 +5170,11 @@ function asRecord(value, path) {
4958
5170
  }
4959
5171
  return value;
4960
5172
  }
4961
- function assertKeys(record2, path, required, optional = []) {
5173
+ function assertKeys(record3, path, required, optional = []) {
4962
5174
  const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
4963
- const missing = required.find((key) => !Object.hasOwn(record2, key));
5175
+ const missing = required.find((key) => !Object.hasOwn(record3, key));
4964
5176
  if (missing !== void 0) contractError(path, `is missing ${missing}`);
4965
- const extra = Object.keys(record2).find((key) => !allowed.has(key));
5177
+ const extra = Object.keys(record3).find((key) => !allowed.has(key));
4966
5178
  if (extra !== void 0) contractError(path, `contains unexpected field ${extra}`);
4967
5179
  }
4968
5180
  function asString(value, path, options) {
@@ -4992,97 +5204,97 @@ function asSha(value, path) {
4992
5204
  if (!SHA_RE.test(sha)) contractError(path, "must be a canonical lowercase Git commit SHA");
4993
5205
  return sha;
4994
5206
  }
4995
- function optionalString(record2, key, path, max) {
4996
- if (!Object.hasOwn(record2, key)) return void 0;
4997
- return asString(record2[key], `${path}.${key}`, { max, nonempty: true });
5207
+ function optionalString(record3, key, path, max) {
5208
+ if (!Object.hasOwn(record3, key)) return void 0;
5209
+ return asString(record3[key], `${path}.${key}`, { max, nonempty: true });
4998
5210
  }
4999
5211
  function parseProposal(value, path) {
5000
- const record2 = asRecord(value, path);
5212
+ const record3 = asRecord(value, path);
5001
5213
  assertKeys(
5002
- record2,
5214
+ record3,
5003
5215
  path,
5004
5216
  ["_id", "repoFullName", "deadCommitSha", "status", "signals", "lastEvaluatedAt"],
5005
5217
  ["proposedSha", "proposedTier", "nextEvaluateAt"]
5006
5218
  );
5007
- const repoFullName = asString(record2.repoFullName, `${path}.repoFullName`, {
5219
+ const repoFullName = asString(record3.repoFullName, `${path}.repoFullName`, {
5008
5220
  max: 200,
5009
5221
  nonempty: true
5010
5222
  });
5011
5223
  if (!REPO_FULL_NAME_RE.test(repoFullName)) {
5012
5224
  contractError(`${path}.repoFullName`, "must be canonical owner/repository syntax");
5013
5225
  }
5014
- const status = record2.status;
5226
+ const status = record3.status;
5015
5227
  if (status !== "proposed" && status !== "confirmed" && status !== "verification_failed") {
5016
5228
  contractError(`${path}.status`, "is not a review-visible proposal status");
5017
5229
  }
5018
- if (!Array.isArray(record2.signals) || record2.signals.length > MAX_SIGNALS) {
5230
+ if (!Array.isArray(record3.signals) || record3.signals.length > MAX_SIGNALS) {
5019
5231
  contractError(`${path}.signals`, "must be a bounded array");
5020
5232
  }
5021
- const signals = record2.signals.map(
5233
+ const signals = record3.signals.map(
5022
5234
  (signal, index) => asString(signal, `${path}.signals[${String(index)}]`, {
5023
5235
  max: MAX_SIGNAL_LENGTH,
5024
5236
  nonempty: true
5025
5237
  })
5026
5238
  );
5027
- const proposedSha = Object.hasOwn(record2, "proposedSha") ? asSha(record2.proposedSha, `${path}.proposedSha`) : void 0;
5239
+ const proposedSha = Object.hasOwn(record3, "proposedSha") ? asSha(record3.proposedSha, `${path}.proposedSha`) : void 0;
5028
5240
  let proposedTier;
5029
- if (Object.hasOwn(record2, "proposedTier")) {
5030
- if (record2.proposedTier !== "exact_patch_body" && record2.proposedTier !== "exact_file_set") {
5241
+ if (Object.hasOwn(record3, "proposedTier")) {
5242
+ if (record3.proposedTier !== "exact_patch_body" && record3.proposedTier !== "exact_file_set") {
5031
5243
  contractError(`${path}.proposedTier`, "is not a supported match tier");
5032
5244
  }
5033
- proposedTier = record2.proposedTier;
5245
+ proposedTier = record3.proposedTier;
5034
5246
  }
5035
- const nextEvaluateAt = Object.hasOwn(record2, "nextEvaluateAt") ? asTimestamp(record2.nextEvaluateAt, `${path}.nextEvaluateAt`) : void 0;
5247
+ const nextEvaluateAt = Object.hasOwn(record3, "nextEvaluateAt") ? asTimestamp(record3.nextEvaluateAt, `${path}.nextEvaluateAt`) : void 0;
5036
5248
  return {
5037
- _id: asString(record2._id, `${path}._id`, { max: 256, nonempty: true }),
5249
+ _id: asString(record3._id, `${path}._id`, { max: 256, nonempty: true }),
5038
5250
  repoFullName,
5039
- deadCommitSha: asSha(record2.deadCommitSha, `${path}.deadCommitSha`),
5251
+ deadCommitSha: asSha(record3.deadCommitSha, `${path}.deadCommitSha`),
5040
5252
  status,
5041
5253
  proposedSha,
5042
5254
  proposedTier,
5043
5255
  signals,
5044
- lastEvaluatedAt: asTimestamp(record2.lastEvaluatedAt, `${path}.lastEvaluatedAt`),
5256
+ lastEvaluatedAt: asTimestamp(record3.lastEvaluatedAt, `${path}.lastEvaluatedAt`),
5045
5257
  nextEvaluateAt
5046
5258
  };
5047
5259
  }
5048
5260
  function parseDecision(value, path) {
5049
- const record2 = asRecord(value, path);
5050
- assertKeys(record2, path, ["_id", "intent", "status", "intentTruncated"], ["shortId"]);
5051
- const status = record2.status;
5261
+ const record3 = asRecord(value, path);
5262
+ assertKeys(record3, path, ["_id", "intent", "status", "intentTruncated"], ["shortId"]);
5263
+ const status = record3.status;
5052
5264
  if (status !== "provisional" && status !== "abandoned") {
5053
5265
  contractError(`${path}.status`, "is not a repairable decision status");
5054
5266
  }
5055
5267
  return {
5056
- _id: asString(record2._id, `${path}._id`, { max: 256, nonempty: true }),
5057
- shortId: optionalString(record2, "shortId", path, 128),
5058
- intent: asString(record2.intent, `${path}.intent`, { max: MAX_INTENT_LENGTH }),
5268
+ _id: asString(record3._id, `${path}._id`, { max: 256, nonempty: true }),
5269
+ shortId: optionalString(record3, "shortId", path, 128),
5270
+ intent: asString(record3.intent, `${path}.intent`, { max: MAX_INTENT_LENGTH }),
5059
5271
  status,
5060
- intentTruncated: asBoolean(record2.intentTruncated, `${path}.intentTruncated`)
5272
+ intentTruncated: asBoolean(record3.intentTruncated, `${path}.intentTruncated`)
5061
5273
  };
5062
5274
  }
5063
5275
  function parseListItem(value, path) {
5064
- const record2 = asRecord(value, path);
5065
- assertKeys(record2, path, [
5276
+ const record3 = asRecord(value, path);
5277
+ assertKeys(record3, path, [
5066
5278
  "proposal",
5067
5279
  "decisions",
5068
5280
  "affectedDecisionCount",
5069
5281
  "decisionsTruncated",
5070
5282
  "reviewToken"
5071
5283
  ]);
5072
- if (!Array.isArray(record2.decisions) || record2.decisions.length > MAX_REPAIR_DECISIONS_FOR_CONFIRM) {
5284
+ if (!Array.isArray(record3.decisions) || record3.decisions.length > MAX_REPAIR_DECISIONS_FOR_CONFIRM) {
5073
5285
  contractError(`${path}.decisions`, "must be a bounded array");
5074
5286
  }
5075
- const decisions = record2.decisions.map(
5287
+ const decisions = record3.decisions.map(
5076
5288
  (decision, index) => parseDecision(decision, `${path}.decisions[${String(index)}]`)
5077
5289
  );
5078
- const proposal = parseProposal(record2.proposal, `${path}.proposal`);
5079
- const decisionsTruncated = asBoolean(record2.decisionsTruncated, `${path}.decisionsTruncated`);
5080
- const affectedDecisionCount = record2.affectedDecisionCount === null ? null : asCount(record2.affectedDecisionCount, `${path}.affectedDecisionCount`);
5290
+ const proposal = parseProposal(record3.proposal, `${path}.proposal`);
5291
+ const decisionsTruncated = asBoolean(record3.decisionsTruncated, `${path}.decisionsTruncated`);
5292
+ const affectedDecisionCount = record3.affectedDecisionCount === null ? null : asCount(record3.affectedDecisionCount, `${path}.affectedDecisionCount`);
5081
5293
  let reviewToken;
5082
- if (record2.reviewToken === null) {
5294
+ if (record3.reviewToken === null) {
5083
5295
  reviewToken = null;
5084
5296
  } else {
5085
- reviewToken = asString(record2.reviewToken, `${path}.reviewToken`, {
5297
+ reviewToken = asString(record3.reviewToken, `${path}.reviewToken`, {
5086
5298
  max: 64,
5087
5299
  nonempty: true
5088
5300
  });
@@ -5115,19 +5327,19 @@ function parseListItem(value, path) {
5115
5327
  }
5116
5328
  function parseListPage(value, pageNumber) {
5117
5329
  const path = `page ${String(pageNumber)}`;
5118
- const record2 = asRecord(value, path);
5119
- assertKeys(record2, path, ["repairs", "isDone", "nextCursor", "truncated"]);
5120
- if (!Array.isArray(record2.repairs) || record2.repairs.length > MAX_REPAIRS_PER_PAGE) {
5330
+ const record3 = asRecord(value, path);
5331
+ assertKeys(record3, path, ["repairs", "isDone", "nextCursor", "truncated"]);
5332
+ if (!Array.isArray(record3.repairs) || record3.repairs.length > MAX_REPAIRS_PER_PAGE) {
5121
5333
  contractError(`${path}.repairs`, "must be a bounded array");
5122
5334
  }
5123
- const isDone = asBoolean(record2.isDone, `${path}.isDone`);
5124
- const truncated = asBoolean(record2.truncated, `${path}.truncated`);
5335
+ const isDone = asBoolean(record3.isDone, `${path}.isDone`);
5336
+ const truncated = asBoolean(record3.truncated, `${path}.truncated`);
5125
5337
  if (truncated === isDone) contractError(path, "has inconsistent isDone/truncated flags");
5126
5338
  let nextCursor;
5127
- if (record2.nextCursor === null) {
5339
+ if (record3.nextCursor === null) {
5128
5340
  nextCursor = null;
5129
5341
  } else {
5130
- nextCursor = asString(record2.nextCursor, `${path}.nextCursor`, {
5342
+ nextCursor = asString(record3.nextCursor, `${path}.nextCursor`, {
5131
5343
  max: MAX_CURSOR_LENGTH,
5132
5344
  nonempty: true
5133
5345
  });
@@ -5135,7 +5347,7 @@ function parseListPage(value, pageNumber) {
5135
5347
  if (isDone && nextCursor !== null) contractError(path, "is done but includes a next cursor");
5136
5348
  if (!isDone && nextCursor === null) contractError(path, "is incomplete without a next cursor");
5137
5349
  return {
5138
- repairs: record2.repairs.map(
5350
+ repairs: record3.repairs.map(
5139
5351
  (item, index) => parseListItem(item, `${path}.repairs[${String(index)}]`)
5140
5352
  ),
5141
5353
  isDone,
@@ -5213,23 +5425,23 @@ function normalizedResolutionInput(proposalId, expectedProposedSha) {
5213
5425
  return { proposalId, expectedProposedSha: expectedProposedSha.toLowerCase() };
5214
5426
  }
5215
5427
  function parseResolutionOutcome(value, action) {
5216
- const record2 = asRecord(value, "resolution response");
5428
+ const record3 = asRecord(value, "resolution response");
5217
5429
  const allowed = action === "confirm" ? ["confirmed", "already_applied", "disabled", "no_op", "stale"] : ["rejected", "already_applied", "no_op", "stale"];
5218
- if (record2.status === "backoff") {
5219
- assertKeys(record2, "resolution response", ["status", "retryAt"]);
5430
+ if (record3.status === "backoff") {
5431
+ assertKeys(record3, "resolution response", ["status", "retryAt"]);
5220
5432
  if (action !== "confirm") {
5221
5433
  contractError("resolution response.status", "backoff is impossible for reject");
5222
5434
  }
5223
5435
  return {
5224
5436
  status: "backoff",
5225
- retryAt: asTimestamp(record2.retryAt, "resolution response.retryAt")
5437
+ retryAt: asTimestamp(record3.retryAt, "resolution response.retryAt")
5226
5438
  };
5227
5439
  }
5228
- assertKeys(record2, "resolution response", ["status"]);
5229
- if (typeof record2.status !== "string" || !allowed.includes(record2.status)) {
5440
+ assertKeys(record3, "resolution response", ["status"]);
5441
+ if (typeof record3.status !== "string" || !allowed.includes(record3.status)) {
5230
5442
  contractError("resolution response.status", "is not a supported outcome");
5231
5443
  }
5232
- return { status: record2.status };
5444
+ return { status: record3.status };
5233
5445
  }
5234
5446
  function normalizedReviewToken(value) {
5235
5447
  if (value === void 0 || !REVIEW_TOKEN_INPUT_RE.test(value)) {
@@ -5240,18 +5452,18 @@ function normalizedReviewToken(value) {
5240
5452
  return value.toLowerCase();
5241
5453
  }
5242
5454
  function parseConflictOutcome(value, action) {
5243
- const record2 = asRecord(value, "resolution conflict response");
5244
- assertKeys(record2, "resolution conflict response", ["status"]);
5455
+ const record3 = asRecord(value, "resolution conflict response");
5456
+ assertKeys(record3, "resolution conflict response", ["status"]);
5245
5457
  if (action !== "confirm") {
5246
5458
  contractError(
5247
5459
  "resolution conflict response.status",
5248
5460
  "a review conflict is impossible for reject"
5249
5461
  );
5250
5462
  }
5251
- if (record2.status !== "review_too_large" && record2.status !== "stale_review") {
5463
+ if (record3.status !== "review_too_large" && record3.status !== "stale_review") {
5252
5464
  contractError("resolution conflict response.status", "is not a supported 409 outcome");
5253
5465
  }
5254
- return { status: record2.status };
5466
+ return { status: record3.status };
5255
5467
  }
5256
5468
  async function resolveRepair(proposalId, expectedProposedSha, action, expectedReviewToken, deps = defaultDeps6) {
5257
5469
  const normalized = normalizedResolutionInput(proposalId, expectedProposedSha);
@@ -6306,6 +6518,7 @@ function registerDecisionsCommands(program2) {
6306
6518
 
6307
6519
  // src/commands/doctor.ts
6308
6520
  import { existsSync as existsSync6 } from "fs";
6521
+ import { Option as Option4 } from "commander";
6309
6522
 
6310
6523
  // src/protocol/decision-lifecycle.ts
6311
6524
  var DECISION_LIFECYCLE_PROTOCOL_VERSION = 2;
@@ -6330,28 +6543,35 @@ function parseOrganizationBinding(value) {
6330
6543
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
6331
6544
  return { state: "server_contract_unavailable" };
6332
6545
  }
6333
- const record2 = value;
6334
- if (record2.authenticated !== true || record2.organizationBindingVersion !== ORGANIZATION_BINDING_VERSION || !isCaptureAuthorityKind(record2.captureAuthorityKind)) {
6546
+ const record3 = value;
6547
+ if (record3.authenticated !== true || record3.organizationBindingVersion !== ORGANIZATION_BINDING_VERSION || !isCaptureAuthorityKind(record3.captureAuthorityKind)) {
6335
6548
  return { state: "server_contract_unavailable" };
6336
6549
  }
6337
- if (record2.organizationId === null && record2.workosOrganizationId === null) {
6550
+ if (record3.organizationId === null && record3.workosOrganizationId === null) {
6338
6551
  return { state: "no_current_organization" };
6339
6552
  }
6340
- if (safeOrganizationId(record2.organizationId) && record2.workosOrganizationId === null) {
6553
+ if (safeOrganizationId(record3.organizationId) && record3.workosOrganizationId === null) {
6341
6554
  return { state: "organization_identity_unreconciled" };
6342
6555
  }
6343
- if (!safeOrganizationId(record2.organizationId) || !safeOrganizationId(record2.workosOrganizationId)) {
6556
+ if (!safeOrganizationId(record3.organizationId) || !safeOrganizationId(record3.workosOrganizationId)) {
6344
6557
  return { state: "server_contract_unavailable" };
6345
6558
  }
6346
6559
  return {
6347
6560
  state: "current",
6348
6561
  binding: {
6349
- captureAuthorityKind: record2.captureAuthorityKind,
6350
- organizationId: record2.organizationId,
6351
- workosOrganizationId: record2.workosOrganizationId
6562
+ captureAuthorityKind: record3.captureAuthorityKind,
6563
+ organizationId: record3.organizationId,
6564
+ workosOrganizationId: record3.workosOrganizationId
6352
6565
  }
6353
6566
  };
6354
6567
  }
6568
+ function summarizeRetainedBuckets(retained) {
6569
+ const counts = /* @__PURE__ */ new Map();
6570
+ for (const item of retained) {
6571
+ counts.set(item.reason, (counts.get(item.reason) ?? 0) + 1);
6572
+ }
6573
+ return [...counts].sort(([left], [right]) => left.localeCompare(right)).map(([reason, count]) => `${reason}:${String(count)}`).join(", ");
6574
+ }
6355
6575
  function classifyJournalBuckets(buckets, binding) {
6356
6576
  const deliverableBuckets = /* @__PURE__ */ new Set();
6357
6577
  const retainedBuckets = [];
@@ -6423,7 +6643,7 @@ async function inspectJournalDelivery(buckets, options = {}) {
6423
6643
  }
6424
6644
 
6425
6645
  // src/commands/hermes-install.ts
6426
- import { existsSync as existsSync5, readFileSync as readFileSync5, rmSync as rmSync2, statSync } from "fs";
6646
+ import { existsSync as existsSync5, readFileSync as readFileSync5, rmSync as rmSync2, statSync as statSync2 } from "fs";
6427
6647
  import { homedir as homedir4 } from "os";
6428
6648
  import { join as join6 } from "path";
6429
6649
  import { Document, isMap, isSeq, parseAllDocuments, stringify, visit } from "yaml";
@@ -6831,7 +7051,7 @@ function readConfigSnapshot(path) {
6831
7051
  if (!existsSync5(path)) return { exists: false, mode: CONFIG_MODE, raw: "" };
6832
7052
  return {
6833
7053
  exists: true,
6834
- mode: statSync(path).mode & 511,
7054
+ mode: statSync2(path).mode & 511,
6835
7055
  raw: readFileSync5(path, "utf8")
6836
7056
  };
6837
7057
  }
@@ -6973,7 +7193,6 @@ function registerHermesCommands(program2) {
6973
7193
  // src/commands/doctor.ts
6974
7194
  var DAEMON_PROBE_TIMEOUT_MS = 500;
6975
7195
  var CONNECTIVITY_TIMEOUT_MS = 3e3;
6976
- var MS_PER_SECOND = 1e3;
6977
7196
  var STALE_PENDING_MS = 3e4;
6978
7197
  var EXIT_UNHEALTHY = 1;
6979
7198
  function classifyDoctor(checks) {
@@ -7095,8 +7314,19 @@ function classifyDaemonHealth(snapshot, options = {}) {
7095
7314
  };
7096
7315
  }
7097
7316
  if (!snapshot.ingestion?.healthy) {
7098
- const pending = snapshot.ingestion?.pendingCount ?? 0;
7099
- const pendingLabel = snapshot.ingestion?.pendingSampled ? `at least ${String(pending)}` : String(pending);
7317
+ if (options.backlogExpected && deliveryBacklogState(snapshot.ingestion) === "draining") {
7318
+ const ingestionStatus2 = options.ingestionStatus ?? decisionIngestionStatus(process.cwd());
7319
+ const live = options.backlog;
7320
+ const progress = live && live.pendingCount === 0 && live.pendingSampled !== true ? "backlog drained; daemon health refreshes on its next sweep" : deliveryBacklogSummary(snapshot.ingestion, options.now, live);
7321
+ return {
7322
+ name: "daemon",
7323
+ status: "warn",
7324
+ detail: `supervised and live${snapshot.version ? ` \xB7 v${snapshot.version}` : ""} \xB7 Decision ingestion ${ingestionStatus2} \xB7 ${progress}`
7325
+ };
7326
+ }
7327
+ const counts = options.backlog ?? snapshot.ingestion;
7328
+ const pending = counts?.pendingCount ?? 0;
7329
+ const pendingLabel = counts?.pendingSampled ? `at least ${String(pending)}` : String(pending);
7100
7330
  return {
7101
7331
  name: "daemon",
7102
7332
  status: "fail",
@@ -7113,17 +7343,23 @@ function classifyDaemonHealth(snapshot, options = {}) {
7113
7343
  detail: `supervised and healthy${snapshot.version ? ` \xB7 v${snapshot.version}` : ""} \xB7 Decision ingestion ${ingestionStatus}`
7114
7344
  };
7115
7345
  }
7116
- async function checkDaemon() {
7346
+ async function probeDelivery() {
7347
+ const stats = pendingJournalStats();
7348
+ const now = Date.now();
7349
+ const disabled = daemonExplicitlyDisabled();
7350
+ const expectedVersion = packageVersion();
7117
7351
  let service;
7118
7352
  if (process.platform === "darwin") {
7119
7353
  try {
7120
7354
  service = getLaunchdService();
7121
7355
  } catch (error) {
7122
- const detail = boundedHealthError(error instanceof Error ? error.message : String(error));
7123
7356
  return {
7124
- name: "daemon",
7125
- status: "fail",
7126
- detail: `launchd status unavailable${detail ? `: ${detail}` : ""}`
7357
+ snapshot: null,
7358
+ launchdError: error instanceof Error ? error.message : String(error),
7359
+ disabled,
7360
+ expectedVersion,
7361
+ stats,
7362
+ now
7127
7363
  };
7128
7364
  }
7129
7365
  }
@@ -7132,53 +7368,74 @@ async function checkDaemon() {
7132
7368
  { callerEnv: getSiteUrl() },
7133
7369
  { timeoutMs: DAEMON_PROBE_TIMEOUT_MS }
7134
7370
  );
7135
- return classifyDaemonHealth(snapshot, {
7136
- disabled: daemonExplicitlyDisabled(),
7137
- service,
7138
- expectedVersion: packageVersion()
7371
+ return { snapshot, service, disabled, expectedVersion, stats, now };
7372
+ }
7373
+ function daemonCheckVouchesForDrain(check) {
7374
+ return check.name === "daemon" && check.status !== "fail";
7375
+ }
7376
+ function classifyDelivery(probe, options = {}) {
7377
+ const { stats, now } = probe;
7378
+ const launchdDetail = probe.launchdError === void 0 ? void 0 : boundedHealthError(probe.launchdError);
7379
+ const daemon = probe.launchdError === void 0 ? classifyDaemonHealth(probe.snapshot, {
7380
+ disabled: probe.disabled,
7381
+ service: probe.service,
7382
+ ingestionStatus: probe.ingestionStatus,
7383
+ expectedVersion: probe.expectedVersion,
7384
+ backlogExpected: options.backlogExpected,
7385
+ backlog: {
7386
+ pendingCount: stats.pendingCount,
7387
+ pendingSampled: stats.sampled,
7388
+ oldestPendingAt: stats.oldestPendingAt
7389
+ },
7390
+ now
7391
+ }) : {
7392
+ name: "daemon",
7393
+ status: "fail",
7394
+ detail: `launchd status unavailable${launchdDetail ? `: ${launchdDetail}` : ""}`
7395
+ };
7396
+ const journal = classifyJournal(stats, now, {
7397
+ backlogExpected: options.backlogExpected === true && daemonCheckVouchesForDrain(daemon)
7139
7398
  });
7399
+ return [daemon, journal, classifyStranded(stats)];
7140
7400
  }
7141
- function checkJournal() {
7142
- const stats = pendingJournalStats();
7401
+ function classifyJournal(stats, now, options = {}) {
7143
7402
  const pending = stats.pendingCount;
7144
7403
  const pendingLabel = stats.sampled ? `at least ${String(pending)}` : String(pending);
7404
+ const backlog = (detail) => options.backlogExpected ? {
7405
+ name: "journal",
7406
+ status: "warn",
7407
+ detail: `${formatPendingBacklog(
7408
+ {
7409
+ pendingCount: pending,
7410
+ pendingSampled: stats.sampled,
7411
+ oldestPendingAt: stats.oldestPendingAt
7412
+ },
7413
+ now
7414
+ )} \u2014 draining in the background`
7415
+ } : { name: "journal", status: "fail", detail };
7145
7416
  if (pending === 0) {
7146
7417
  if (stats.sampled) {
7147
- return {
7148
- name: "journal",
7149
- status: "fail",
7150
- detail: "bounded journal sample could not prove the queue is empty"
7151
- };
7418
+ return backlog("bounded journal sample could not prove the queue is empty");
7152
7419
  }
7153
7420
  return { name: "journal", status: "ok", detail: "no pending moves" };
7154
7421
  }
7155
7422
  if (stats.oldestPendingAt === void 0) {
7156
- return {
7157
- name: "journal",
7158
- status: "fail",
7159
- detail: `${pendingLabel} pending with no readable capture timestamp`
7160
- };
7423
+ const detail = `${pendingLabel} pending with no readable capture timestamp`;
7424
+ return stats.sampled ? backlog(detail) : { name: "journal", status: "fail", detail };
7161
7425
  }
7162
- const oldestMs = Date.now() - stats.oldestPendingAt;
7426
+ const oldestMs = now - stats.oldestPendingAt;
7163
7427
  const oldestS = Math.round(oldestMs / MS_PER_SECOND);
7164
7428
  if (oldestMs > STALE_PENDING_MS) {
7165
- return {
7166
- name: "journal",
7167
- status: "fail",
7168
- detail: `${pendingLabel} pending, oldest observed ${String(oldestS)}s \u2014 30s delivery SLA missed`
7169
- };
7429
+ return backlog(
7430
+ `${pendingLabel} pending, oldest observed ${String(oldestS)}s \u2014 30s delivery SLA missed`
7431
+ );
7170
7432
  }
7171
7433
  if (stats.sampled) {
7172
- return {
7173
- name: "journal",
7174
- status: "fail",
7175
- detail: `${pendingLabel} pending in bounded sample; 30s delivery SLA cannot be proven`
7176
- };
7434
+ return backlog(`${pendingLabel} pending in bounded sample; 30s delivery SLA cannot be proven`);
7177
7435
  }
7178
7436
  return { name: "journal", status: "ok", detail: `${String(pending)} pending, draining` };
7179
7437
  }
7180
- function checkStranded() {
7181
- const stats = pendingJournalStats();
7438
+ function classifyStranded(stats) {
7182
7439
  if (stats.strandedFileCount === 0 && !stats.strandedSampled) {
7183
7440
  return { name: "stranded", status: "ok", detail: "none" };
7184
7441
  }
@@ -7204,15 +7461,10 @@ function classifyJournalOrganization(bucketCount, retainedBuckets) {
7204
7461
  detail: "all pending buckets match the active credential"
7205
7462
  };
7206
7463
  }
7207
- const reasonCounts = /* @__PURE__ */ new Map();
7208
- for (const item of retainedBuckets) {
7209
- reasonCounts.set(item.reason, (reasonCounts.get(item.reason) ?? 0) + 1);
7210
- }
7211
- const reasons = [...reasonCounts].sort(([left], [right]) => left.localeCompare(right)).map(([reason, count]) => `${reason}:${String(count)}`).join(", ");
7212
7464
  return {
7213
7465
  name: "journal-org",
7214
7466
  status: "fail",
7215
- detail: `${String(retainedBuckets.length)} bucket(s) retained (${reasons})`
7467
+ detail: `${String(retainedBuckets.length)} bucket(s) retained (${summarizeRetainedBuckets(retainedBuckets)})`
7216
7468
  };
7217
7469
  }
7218
7470
  async function checkJournalOrganization() {
@@ -7623,30 +7875,37 @@ function refreshOwnedGlobalHooksForHealth() {
7623
7875
  } catch {
7624
7876
  }
7625
7877
  }
7626
- async function collectChecks() {
7878
+ async function independentChecks() {
7627
7879
  refreshOwnedGlobalHooksForHealth();
7628
7880
  const backend = await checkBackend();
7629
- return [
7630
- checkAuth(),
7631
- await checkDaemon(),
7632
- checkJournal(),
7633
- checkStranded(),
7634
- await checkJournalOrganization(),
7635
- checkFeedbackHooks(),
7636
- ...checkAgentHooks(),
7637
- checkHookRuntime(),
7638
- await checkRepositoryBinding(),
7639
- checkManagedHook("post-commit", inspectEffectivePostCommitHook),
7640
- checkManagedHook("post-rewrite", inspectEffectivePostRewriteHook),
7641
- ...backend,
7642
- await checkFeedbackCapability()
7643
- ];
7881
+ return {
7882
+ before: [checkAuth()],
7883
+ after: [
7884
+ await checkJournalOrganization(),
7885
+ checkFeedbackHooks(),
7886
+ ...checkAgentHooks(),
7887
+ checkHookRuntime(),
7888
+ await checkRepositoryBinding(),
7889
+ checkManagedHook("post-commit", inspectEffectivePostCommitHook),
7890
+ checkManagedHook("post-rewrite", inspectEffectivePostRewriteHook),
7891
+ ...backend,
7892
+ await checkFeedbackCapability()
7893
+ ]
7894
+ };
7895
+ }
7896
+ var DEFAULT_DOCTOR_PROBES = {
7897
+ delivery: probeDelivery,
7898
+ independent: independentChecks
7899
+ };
7900
+ async function collectChecks(options, probes) {
7901
+ const { before, after } = await probes.independent();
7902
+ return [...before, ...classifyDelivery(await probes.delivery(), options), ...after];
7644
7903
  }
7645
7904
  function icon(status) {
7646
7905
  return status === "ok" ? "\u2713" : status === "warn" ? "\u26A0" : "\u2717";
7647
7906
  }
7648
- async function runDoctor() {
7649
- const checks = await collectChecks();
7907
+ async function runDoctor(options, probes) {
7908
+ const checks = await collectChecks(options, probes);
7650
7909
  const { json, exitCode } = classifyDoctor(checks);
7651
7910
  const headline = json.status === "ok" ? "\u2713 healthy" : json.status === "warn" ? "\u26A0 degraded" : "\u2717 unhealthy";
7652
7911
  process.stderr.write(`[prim] doctor: ${headline}
@@ -7660,11 +7919,14 @@ async function runDoctor() {
7660
7919
  process.exitCode = exitCode;
7661
7920
  }
7662
7921
  }
7663
- function registerDoctorCommands(program2) {
7922
+ function registerDoctorCommands(program2, dependencies = {}) {
7664
7923
  program2.command("doctor").description(
7665
7924
  "Check capture and feedback health end to end (auth, supervisor, delivery, server)"
7666
- ).action(async () => {
7667
- await runDoctor();
7925
+ ).addOption(new Option4("--expect-backlog").hideHelp()).action(async (opts) => {
7926
+ await runDoctor(
7927
+ { backlogExpected: opts.expectBacklog === true },
7928
+ dependencies.probes ?? DEFAULT_DOCTOR_PROBES
7929
+ );
7668
7930
  });
7669
7931
  }
7670
7932
 
@@ -7673,20 +7935,20 @@ import { existsSync as existsSync8, mkdirSync as mkdirSync5, unlinkSync as unlin
7673
7935
  import { join as join9 } from "path";
7674
7936
 
7675
7937
  // src/flusher.ts
7676
- import { createReadStream, renameSync as renameSync3, unlinkSync as unlinkSync3 } from "fs";
7938
+ import { createReadStream, renameSync as renameSync3, statSync as statSync3, unlinkSync as unlinkSync3 } from "fs";
7677
7939
  import { dirname as dirname4, join as join8 } from "path";
7678
7940
 
7679
7941
  // src/dead-letter.ts
7680
7942
  import { createHash as createHash3, randomBytes as randomBytes4 } from "crypto";
7681
7943
  import {
7682
- constants as constants2,
7944
+ constants as constants3,
7683
7945
  chmodSync as chmodSync2,
7684
- closeSync as closeSync2,
7946
+ closeSync as closeSync3,
7685
7947
  existsSync as existsSync7,
7686
7948
  fchmodSync,
7687
7949
  fsyncSync,
7688
7950
  mkdirSync as mkdirSync4,
7689
- openSync as openSync2,
7951
+ openSync as openSync3,
7690
7952
  renameSync as renameSync2,
7691
7953
  rmSync as rmSync3,
7692
7954
  writeFileSync as writeFileSync3
@@ -7709,7 +7971,7 @@ function ensureDeadLetterDirectory(flushingPath, persistDirectory) {
7709
7971
  persistDirectory(dirname3(directory));
7710
7972
  return directory;
7711
7973
  }
7712
- function persistDeadLetter(directory, path, quarantineId, record2, persistDirectory) {
7974
+ function persistDeadLetter(directory, path, quarantineId, record3, persistDirectory) {
7713
7975
  if (existsSync7(path)) {
7714
7976
  persistDirectory(directory);
7715
7977
  return;
@@ -7718,16 +7980,16 @@ function persistDeadLetter(directory, path, quarantineId, record2, persistDirect
7718
7980
  directory,
7719
7981
  `.${quarantineId}.${String(process.pid)}.${randomBytes4(8).toString("hex")}.tmp`
7720
7982
  );
7721
- const flags = constants2.O_CREAT | constants2.O_EXCL | constants2.O_WRONLY | constants2.O_NOFOLLOW;
7983
+ const flags = constants3.O_CREAT | constants3.O_EXCL | constants3.O_WRONLY | constants3.O_NOFOLLOW;
7722
7984
  try {
7723
- const fd = openSync2(temporaryPath, flags, FILE_MODE);
7985
+ const fd = openSync3(temporaryPath, flags, FILE_MODE);
7724
7986
  try {
7725
7987
  fchmodSync(fd, FILE_MODE);
7726
- writeFileSync3(fd, `${JSON.stringify(record2)}
7988
+ writeFileSync3(fd, `${JSON.stringify(record3)}
7727
7989
  `, "utf8");
7728
7990
  fsyncSync(fd);
7729
7991
  } finally {
7730
- closeSync2(fd);
7992
+ closeSync3(fd);
7731
7993
  }
7732
7994
  renameSync2(temporaryPath, path);
7733
7995
  persistDirectory(directory);
@@ -7739,22 +8001,22 @@ function quarantineMove(flushingPath, move, reason, now = Date.now, options = {}
7739
8001
  const persistDirectory = options.syncDirectory ?? syncDirectory;
7740
8002
  const directory = ensureDeadLetterDirectory(flushingPath, persistDirectory);
7741
8003
  const serializedMove = JSON.stringify(move);
7742
- const record2 = {
8004
+ const record3 = {
7743
8005
  version: 1,
7744
8006
  quarantineId: createHash3("sha256").update(serializedMove).digest("hex"),
7745
8007
  quarantinedAt: now(),
7746
8008
  reason,
7747
8009
  move
7748
8010
  };
7749
- const path = join7(directory, `${record2.quarantineId}.json`);
7750
- persistDeadLetter(directory, path, record2.quarantineId, record2, persistDirectory);
7751
- return record2;
8011
+ const path = join7(directory, `${record3.quarantineId}.json`);
8012
+ persistDeadLetter(directory, path, record3.quarantineId, record3, persistDirectory);
8013
+ return record3;
7752
8014
  }
7753
8015
  function quarantineRawLine(flushingPath, rawLine, now = Date.now, options = {}) {
7754
8016
  const persistDirectory = options.syncDirectory ?? syncDirectory;
7755
8017
  const directory = ensureDeadLetterDirectory(flushingPath, persistDirectory);
7756
8018
  const bytes = Buffer.from(rawLine);
7757
- const record2 = {
8019
+ const record3 = {
7758
8020
  version: 1,
7759
8021
  recordKind: "raw_line_v1",
7760
8022
  quarantineId: createHash3("sha256").update(bytes).digest("hex"),
@@ -7767,18 +8029,22 @@ function quarantineRawLine(flushingPath, rawLine, now = Date.now, options = {})
7767
8029
  persistDeadLetter(
7768
8030
  directory,
7769
8031
  deadLetterPathForRawLine(flushingPath, bytes),
7770
- record2.quarantineId,
7771
- record2,
8032
+ record3.quarantineId,
8033
+ record3,
7772
8034
  persistDirectory
7773
8035
  );
7774
- return record2;
8036
+ return record3;
7775
8037
  }
7776
8038
 
7777
8039
  // src/flusher.ts
7778
8040
  var BATCH_SIZE = 500;
8041
+ var BATCH_MAX_BYTES = 1048576;
7779
8042
  var HTTP_TIMEOUT_MS = 1e4;
7780
8043
  var OPPORTUNISTIC_FLUSH_AFTER_MS = 6e4;
7781
8044
  var ORPHAN_QUARANTINE_MS = 6e4;
8045
+ function exceedsBatchBytes(count, bytes, nextBytes, maxBytes) {
8046
+ return count > 0 && bytes + nextBytes > maxBytes;
8047
+ }
7782
8048
  function addDrainCounts(left, right) {
7783
8049
  return {
7784
8050
  flushed: left.flushed + right.flushed,
@@ -7789,8 +8055,8 @@ function collectionScopeRoots(batch) {
7789
8055
  return batch.flatMap((move) => {
7790
8056
  const env = move.env;
7791
8057
  if (typeof env !== "object" || env === null || Array.isArray(env)) return [];
7792
- const record2 = env;
7793
- const root = typeof record2.gitRoot === "string" ? record2.gitRoot : record2.cwd;
8058
+ const record3 = env;
8059
+ const root = typeof record3.gitRoot === "string" ? record3.gitRoot : record3.cwd;
7794
8060
  return typeof root === "string" && root.length > 0 ? [root] : [];
7795
8061
  });
7796
8062
  }
@@ -7798,8 +8064,8 @@ function isVersionedInvalidMoveDisposition(body) {
7798
8064
  if (typeof body !== "object" || body === null || Array.isArray(body)) {
7799
8065
  return false;
7800
8066
  }
7801
- const record2 = body;
7802
- return Object.keys(record2).length === 2 && record2.error === "invalid_move" && record2.errorVersion === 1;
8067
+ const record3 = body;
8068
+ return Object.keys(record3).length === 2 && record3.error === "invalid_move" && record3.errorVersion === 1;
7803
8069
  }
7804
8070
  function deadLetterReason(error) {
7805
8071
  if (error instanceof InvalidJournalEnvelopeError) {
@@ -7822,23 +8088,31 @@ function deadLetterReason(error) {
7822
8088
  }
7823
8089
  return void 0;
7824
8090
  }
7825
- async function* rawJournalLines(input) {
8091
+ function mayBeBatchSizeFailure(error) {
8092
+ if (error instanceof HttpError) {
8093
+ return error.status >= 500 && error.status !== 503;
8094
+ }
8095
+ return error instanceof Error && error.name === "TimeoutError";
8096
+ }
8097
+ async function* rawJournalLines(input, offset) {
7826
8098
  let pending = [];
7827
8099
  let pendingBytes = 0;
8100
+ let end = offset;
7828
8101
  for await (const value of input) {
7829
8102
  const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
7830
8103
  let start = 0;
7831
8104
  let newline = chunk.indexOf(10, start);
7832
8105
  while (newline !== -1) {
7833
8106
  const tail = chunk.subarray(start, newline + 1);
7834
- if (pending.length === 0) {
7835
- yield tail;
7836
- } else {
8107
+ let line = tail;
8108
+ if (pending.length > 0) {
7837
8109
  pending.push(tail);
7838
- yield Buffer.concat(pending, pendingBytes + tail.length);
8110
+ line = Buffer.concat(pending, pendingBytes + tail.length);
7839
8111
  pending = [];
7840
8112
  pendingBytes = 0;
7841
8113
  }
8114
+ end += line.length;
8115
+ yield { bytes: line, end };
7842
8116
  start = newline + 1;
7843
8117
  newline = chunk.indexOf(10, start);
7844
8118
  }
@@ -7849,21 +8123,46 @@ async function* rawJournalLines(input) {
7849
8123
  }
7850
8124
  }
7851
8125
  if (pendingBytes > 0) {
7852
- yield pending.length === 1 ? pending[0] : Buffer.concat(pending, pendingBytes);
8126
+ const bytes = pending.length === 1 ? pending[0] : Buffer.concat(pending, pendingBytes);
8127
+ yield { bytes, end: end + pendingBytes };
7853
8128
  }
7854
8129
  }
8130
+ var drainProgressWarned = false;
7855
8131
  async function drainFlushingPath(flushingPath, client, binding, options = {}) {
8132
+ const rotation = rotationIdentity(statSync3(flushingPath));
8133
+ const resumeAt = drainResumeOffset(flushingPath, rotation);
8134
+ const recordProgress = (end) => {
8135
+ if (end === void 0 || end >= rotation.size) {
8136
+ return;
8137
+ }
8138
+ try {
8139
+ writeDrainCheckpoint(flushingPath, { v: 2, offset: end, ...rotation });
8140
+ drainProgressWarned = false;
8141
+ } catch (error) {
8142
+ if (!drainProgressWarned) {
8143
+ drainProgressWarned = true;
8144
+ process.stderr.write(
8145
+ `[prim] could not record drain progress: ${error instanceof Error ? error.message : String(error)}
8146
+ `
8147
+ );
8148
+ }
8149
+ }
8150
+ };
8151
+ const reportProgress = (delta) => {
8152
+ options.onProgress?.(delta);
8153
+ return delta;
8154
+ };
7856
8155
  const postBatch = async (batch2) => {
8156
+ const moves = batch2.map((entry) => entry.move);
7857
8157
  try {
7858
- const request = buildOrganizationBoundMoveRequest(batch2, binding);
8158
+ const request = buildOrganizationBoundMoveRequest(moves, binding);
7859
8159
  const response = await client.post("/api/cli/moves/ingest", request, {
7860
8160
  signal: AbortSignal.timeout(HTTP_TIMEOUT_MS)
7861
8161
  });
7862
- requireDurableIngestAcknowledgement(response, batch2.length, collectionScopeRoots(batch2));
7863
- return { flushed: batch2.length, quarantined: 0 };
8162
+ requireDurableIngestAcknowledgement(response, moves.length, collectionScopeRoots(moves));
7864
8163
  } catch (error) {
7865
8164
  const reason = deadLetterReason(error);
7866
- if (!reason) {
8165
+ if (!(reason || mayBeBatchSizeFailure(error))) {
7867
8166
  throw error;
7868
8167
  }
7869
8168
  if (batch2.length > 1) {
@@ -7872,33 +8171,46 @@ async function drainFlushingPath(flushingPath, client, binding, options = {}) {
7872
8171
  const right = await postBatch(batch2.slice(midpoint));
7873
8172
  return addDrainCounts(left, right);
7874
8173
  }
7875
- const [move] = batch2;
8174
+ if (!reason) {
8175
+ throw error;
8176
+ }
8177
+ const [entry] = batch2;
7876
8178
  const quarantined = quarantineMove(
7877
8179
  flushingPath,
7878
- move,
8180
+ entry.move,
7879
8181
  reason,
7880
8182
  Date.now,
7881
8183
  options.deadLetterPersistence
7882
8184
  );
8185
+ recordProgress(entry.end);
7883
8186
  process.stderr.write(
7884
8187
  `[prim] quarantined rejected move ${quarantined.quarantineId.slice(0, 12)} (${reason})
7885
8188
  `
7886
8189
  );
7887
- return { flushed: 0, quarantined: 1 };
8190
+ return reportProgress({ flushed: 0, quarantined: 1 });
7888
8191
  }
8192
+ recordProgress(batch2.at(-1)?.end);
8193
+ return reportProgress({ flushed: batch2.length, quarantined: 0 });
7889
8194
  };
7890
- const input = createReadStream(flushingPath);
8195
+ const input = createReadStream(flushingPath, { start: resumeAt });
7891
8196
  const decoder = new TextDecoder("utf-8", { fatal: true });
7892
8197
  let batch = [];
8198
+ let batchBytes = 0;
7893
8199
  let counts = { flushed: 0, quarantined: 0 };
8200
+ const postPending = async () => {
8201
+ counts = addDrainCounts(counts, await postBatch(batch));
8202
+ batch = [];
8203
+ batchBytes = 0;
8204
+ };
7894
8205
  try {
7895
- for await (const rawLine of rawJournalLines(input)) {
8206
+ for await (const { bytes: rawLine, end } of rawJournalLines(input, resumeAt)) {
7896
8207
  if (rawLine.length === 1 && rawLine[0] === 10 || rawLine.length === 2 && rawLine[0] === 13 && rawLine[1] === 10) {
7897
8208
  continue;
7898
8209
  }
8210
+ let move;
7899
8211
  try {
7900
8212
  const line = decoder.decode(rawLine);
7901
- batch.push(JSON.parse(line));
8213
+ move = JSON.parse(line);
7902
8214
  } catch {
7903
8215
  const quarantined = quarantineRawLine(
7904
8216
  flushingPath,
@@ -7910,24 +8222,35 @@ async function drainFlushingPath(flushingPath, client, binding, options = {}) {
7910
8222
  `[prim] quarantined invalid journal line ${quarantined.quarantineId.slice(0, 12)} (invalid_move)
7911
8223
  `
7912
8224
  );
7913
- counts.quarantined += 1;
8225
+ counts = addDrainCounts(counts, reportProgress({ flushed: 0, quarantined: 1 }));
8226
+ if (batch.length === 0) {
8227
+ recordProgress(end);
8228
+ }
7914
8229
  continue;
7915
8230
  }
7916
- if (batch.length === BATCH_SIZE) {
7917
- counts = addDrainCounts(counts, await postBatch(batch));
7918
- batch = [];
8231
+ if (exceedsBatchBytes(batch.length, batchBytes, rawLine.length, BATCH_MAX_BYTES)) {
8232
+ await postPending();
8233
+ }
8234
+ batch.push({ move, end });
8235
+ batchBytes += rawLine.length;
8236
+ if (batch.length >= BATCH_SIZE) {
8237
+ await postPending();
7919
8238
  }
7920
8239
  }
7921
8240
  if (batch.length > 0) {
7922
- counts = addDrainCounts(counts, await postBatch(batch));
8241
+ await postPending();
7923
8242
  }
7924
8243
  } finally {
7925
8244
  input.destroy();
7926
8245
  }
7927
8246
  unlinkSync3(flushingPath);
8247
+ try {
8248
+ removeDrainCheckpoint(flushingPath);
8249
+ } catch {
8250
+ }
7928
8251
  return counts;
7929
8252
  }
7930
- async function drainPath(path, client, binding) {
8253
+ async function drainPath(path, client, binding, onProgress) {
7931
8254
  const tmpPath = `${path}.flushing.${String(Date.now())}.${String(process.pid)}`;
7932
8255
  try {
7933
8256
  renameSync3(path, tmpPath);
@@ -7937,7 +8260,19 @@ async function drainPath(path, client, binding) {
7937
8260
  }
7938
8261
  throw err;
7939
8262
  }
7940
- return drainFlushingPath(tmpPath, client, binding);
8263
+ return drainFlushingPath(tmpPath, client, binding, { onProgress });
8264
+ }
8265
+ async function settleDrain(drain) {
8266
+ let progress = { flushed: 0, quarantined: 0 };
8267
+ try {
8268
+ return {
8269
+ counts: await drain((delta) => {
8270
+ progress = addDrainCounts(progress, delta);
8271
+ })
8272
+ };
8273
+ } catch (error) {
8274
+ return { counts: progress, failure: { error } };
8275
+ }
7941
8276
  }
7942
8277
  function selectRecoverable(files, now, opts = {}) {
7943
8278
  const quarantineMs = opts.quarantineMs ?? ORPHAN_QUARANTINE_MS;
@@ -7954,7 +8289,8 @@ async function recoverOrphans(candidates, options) {
7954
8289
  flushed: 0,
7955
8290
  quarantined: 0,
7956
8291
  errors: [],
7957
- failedBuckets: /* @__PURE__ */ new Set()
8292
+ failedBuckets: /* @__PURE__ */ new Set(),
8293
+ failedDrainFlushed: 0
7958
8294
  };
7959
8295
  const recoverable = selectRecoverable(candidates, options.now ?? Date.now(), {
7960
8296
  ownerPid: options.ownerPid ?? process.pid,
@@ -7965,28 +8301,41 @@ async function recoverOrphans(candidates, options) {
7965
8301
  if (summary.failedBuckets.has(file.bucket)) {
7966
8302
  continue;
7967
8303
  }
7968
- try {
7969
- const counts = await drain(file.path);
7970
- summary.flushed += counts.flushed;
7971
- summary.quarantined += counts.quarantined;
7972
- } catch (err) {
7973
- summary.errors.push(err);
8304
+ const { counts, failure: failure4 } = await settleDrain((onProgress) => drain(file.path, onProgress));
8305
+ summary.flushed += counts.flushed;
8306
+ summary.quarantined += counts.quarantined;
8307
+ if (failure4) {
8308
+ summary.failedDrainFlushed += counts.flushed;
8309
+ summary.errors.push(failure4.error);
7974
8310
  summary.failedBuckets.add(file.bucket);
7975
8311
  }
7976
8312
  }
7977
8313
  return summary;
7978
8314
  }
7979
8315
  var FlushError = class extends Error {
8316
+ /** Moves the whole sweep acknowledged, across every drain it ran. */
7980
8317
  flushed;
7981
8318
  quarantined;
7982
- constructor(cause, flushed, quarantined) {
8319
+ /** Buckets the failed sweep held back without sending, as on success. */
8320
+ retained;
8321
+ /**
8322
+ * Of `flushed`, the Moves acknowledged by the drains that failed, before
8323
+ * they failed. Only this shows the failure itself is advancing: `flushed`
8324
+ * also counts other buckets' drains, so a bucket whose drain fails every
8325
+ * time would read as progress while a sibling bucket kept delivering.
8326
+ */
8327
+ failedDrainFlushed;
8328
+ constructor(cause, flushed, quarantined, retained = [], failedDrainFlushed = 0) {
7983
8329
  super(cause instanceof Error ? cause.message : String(cause), { cause });
7984
8330
  this.name = "FlushError";
7985
8331
  this.flushed = flushed;
7986
8332
  this.quarantined = quarantined;
8333
+ this.retained = retained;
8334
+ this.failedDrainFlushed = failedDrainFlushed;
7987
8335
  }
7988
8336
  };
7989
8337
  async function flushOnce() {
8338
+ sweepOrphanedDrainProgress();
7990
8339
  const orphanCandidates = listFlushing({ sampleBytes: 0 });
7991
8340
  const liveBuckets = listBuckets();
7992
8341
  if (orphanCandidates.length === 0 && liveBuckets.length === 0) {
@@ -8003,10 +8352,11 @@ async function flushOnce() {
8003
8352
  const binding = inspection.binding;
8004
8353
  const recovered = await recoverOrphans(
8005
8354
  orphanCandidates.filter((file) => inspection.deliverableBuckets.has(file.bucket)),
8006
- { drain: (path) => drainFlushingPath(path, client, binding) }
8355
+ { drain: (path, onProgress) => drainFlushingPath(path, client, binding, { onProgress }) }
8007
8356
  );
8008
8357
  let total = recovered.flushed;
8009
8358
  let quarantined = recovered.quarantined;
8359
+ let failedDrainFlushed = recovered.failedDrainFlushed;
8010
8360
  const errors = recovered.errors;
8011
8361
  for (const { bucket, path } of liveBuckets) {
8012
8362
  if (!inspection.deliverableBuckets.has(bucket)) {
@@ -8015,16 +8365,24 @@ async function flushOnce() {
8015
8365
  if (recovered.failedBuckets.has(bucket)) {
8016
8366
  continue;
8017
8367
  }
8018
- try {
8019
- const counts = await drainPath(path, client, binding);
8020
- total += counts.flushed;
8021
- quarantined += counts.quarantined;
8022
- } catch (err) {
8023
- errors.push(err);
8368
+ const { counts, failure: failure4 } = await settleDrain(
8369
+ (onProgress) => drainPath(path, client, binding, onProgress)
8370
+ );
8371
+ total += counts.flushed;
8372
+ quarantined += counts.quarantined;
8373
+ if (failure4) {
8374
+ failedDrainFlushed += counts.flushed;
8375
+ errors.push(failure4.error);
8024
8376
  }
8025
8377
  }
8026
8378
  if (errors.length > 0) {
8027
- throw new FlushError(errors[0], total, quarantined);
8379
+ throw new FlushError(
8380
+ errors[0],
8381
+ total,
8382
+ quarantined,
8383
+ inspection.retainedBuckets,
8384
+ failedDrainFlushed
8385
+ );
8028
8386
  }
8029
8387
  return inspection.retainedBuckets.length > 0 ? { flushed: total, quarantined, retained: inspection.retainedBuckets } : { flushed: total, quarantined };
8030
8388
  }
@@ -8054,18 +8412,16 @@ function flush() {
8054
8412
  return attempt;
8055
8413
  }
8056
8414
  function shouldFlushPending(stats, now, thresholdMs = OPPORTUNISTIC_FLUSH_AFTER_MS) {
8057
- if (stats.sampled) {
8415
+ if (stats.sampled || stats.strandedFileCount > 0 && stats.strandedCount === 0) {
8058
8416
  return true;
8059
8417
  }
8060
8418
  return stats.pendingCount > 0 && (stats.oldestPendingAt === void 0 || now - stats.oldestPendingAt > thresholdMs);
8061
8419
  }
8062
- async function flushIfNeeded() {
8420
+ function journalNeedsFlush(now = Date.now()) {
8063
8421
  try {
8064
- const stats = pendingJournalStats();
8065
- if (shouldFlushPending(stats, Date.now())) {
8066
- await flush();
8067
- }
8422
+ return shouldFlushPending(pendingJournalStats(), now);
8068
8423
  } catch {
8424
+ return false;
8069
8425
  }
8070
8426
  }
8071
8427
 
@@ -8124,7 +8480,7 @@ function registerMovesCommands(program2) {
8124
8480
  }
8125
8481
  if (stranded.length > 0) {
8126
8482
  const moveCount = stranded.reduce((n, f) => n + f.lineCount, 0);
8127
- const byteCount = stranded.reduce((n, f) => n + f.sizeBytes, 0);
8483
+ const byteCount = stranded.reduce((n, f) => n + f.pendingBytes, 0);
8128
8484
  const sampled = stranded.some((file) => file.sampled);
8129
8485
  const qualifier = sampled ? "at least " : "";
8130
8486
  console.log(
@@ -8135,7 +8491,7 @@ function registerMovesCommands(program2) {
8135
8491
  const owner = f.pid === void 0 ? "no pid" : `pid ${String(f.pid)}`;
8136
8492
  const count = `${String(f.lineCount)}${f.sampled ? "+" : ""}`;
8137
8493
  console.log(
8138
- ` ${f.bucket.padEnd(BUCKET_COL_WIDTH)} ${count.padStart(5)} stranded, ${String(f.sizeBytes).padStart(8)} bytes, ${String(ageS)}s ago (${owner})`
8494
+ ` ${f.bucket.padEnd(BUCKET_COL_WIDTH)} ${count.padStart(5)} stranded, ${String(f.pendingBytes).padStart(8)} bytes, ${String(ageS)}s ago (${owner})`
8139
8495
  );
8140
8496
  }
8141
8497
  }
@@ -8333,6 +8689,20 @@ import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
8333
8689
  import { join as join11 } from "path";
8334
8690
  var EXIT_INCOMPLETE = 1;
8335
8691
  var EXIT_USAGE3 = 2;
8692
+ var SETUP_ORCHESTRATOR_ENV = "PRIM_SETUP_ORCHESTRATOR";
8693
+ var SETUP_DAEMON_DRAINS_ENV = "PRIM_SETUP_DAEMON_DRAINS";
8694
+ function setupStepSpawnOptions(capture, env = process.env, options = {}) {
8695
+ return {
8696
+ env: {
8697
+ ...env,
8698
+ [SETUP_ORCHESTRATOR_ENV]: "1",
8699
+ // Unset rather than inherited when this setup starts no daemon.
8700
+ [SETUP_DAEMON_DRAINS_ENV]: options.startsDaemon === true ? "1" : void 0
8701
+ },
8702
+ stdio: capture ? ["inherit", "pipe", "ignore"] : "inherit",
8703
+ encoding: "utf-8"
8704
+ };
8705
+ }
8336
8706
  var SESSION_LABELS = {
8337
8707
  claude: "Claude Code integration",
8338
8708
  codex: "Codex integration",
@@ -8383,7 +8753,7 @@ function planSetupSteps(opts) {
8383
8753
  steps.push({
8384
8754
  key: "health",
8385
8755
  label: "Capture health",
8386
- args: ["doctor"],
8756
+ args: ["doctor", "--expect-backlog"],
8387
8757
  required: true
8388
8758
  });
8389
8759
  }
@@ -8483,10 +8853,11 @@ function registerSetupCommand(program2, dependencies = {}) {
8483
8853
  const self = process.argv[1];
8484
8854
  const globals = program2.optsWithGlobals();
8485
8855
  const run = dependencies.run ?? ((args, capture = false) => {
8486
- const r = spawnSync2(process.execPath, [self, ...args], {
8487
- stdio: capture ? ["inherit", "pipe", "ignore"] : "inherit",
8488
- encoding: "utf-8"
8489
- });
8856
+ const r = spawnSync2(
8857
+ process.execPath,
8858
+ [self, ...args],
8859
+ setupStepSpawnOptions(capture, process.env, { startsDaemon: opts.daemon })
8860
+ );
8490
8861
  return { code: r.status ?? 1, stdout: capture ? r.stdout ?? "" : "" };
8491
8862
  });
8492
8863
  const results = {};
@@ -9007,28 +9378,233 @@ function registerWelcomeCommand(program2, deps = { getClient }) {
9007
9378
  });
9008
9379
  }
9009
9380
 
9381
+ // src/daemon/drift-heal.ts
9382
+ var ATTEMPT_MARKER_MODE = 384;
9383
+ var ATTEMPT_MARKER_MAX_BYTES = 1024;
9384
+ var PATH_SEPARATORS = /[\\/]/u;
9385
+ var DAEMON_DRIFT_HEAL_ENV = "PRIM_DAEMON_DRIFT_HEAL";
9386
+ var DAEMON_DRIFT_HEAL_RETRY_MS = 60 * 60 * 1e3;
9387
+ function readAttempt(path) {
9388
+ const file = readBoundedRegularFile(path, ATTEMPT_MARKER_MAX_BYTES);
9389
+ if (!file) return null;
9390
+ try {
9391
+ const value = JSON.parse(file.text);
9392
+ if (typeof value !== "object" || value === null) return null;
9393
+ const attempt = value;
9394
+ return typeof attempt.attemptedAt === "number" && Number.isFinite(attempt.attemptedAt) && typeof attempt.fromVersion === "string" && typeof attempt.toVersion === "string" ? attempt : null;
9395
+ } catch {
9396
+ return null;
9397
+ }
9398
+ }
9399
+ function claimHeld() {
9400
+ throw new Error("another command holds the daemon drift-heal claim");
9401
+ }
9402
+ function claimAttempt(markerPath2, attempt) {
9403
+ const previous = readAttempt(markerPath2);
9404
+ const elapsed = previous ? attempt.attemptedAt - previous.attemptedAt : Number.POSITIVE_INFINITY;
9405
+ if (elapsed >= 0 && elapsed < DAEMON_DRIFT_HEAL_RETRY_MS) return false;
9406
+ atomicWriteFile(markerPath2, `${JSON.stringify(attempt)}
9407
+ `, { mode: ATTEMPT_MARKER_MODE });
9408
+ return true;
9409
+ }
9410
+ function isInstalledPackage(root) {
9411
+ return root?.split(PATH_SEPARATORS).includes("node_modules") === true;
9412
+ }
9413
+ function needsHeal(launcher, toVersion) {
9414
+ const order = compareSemver(launcher.runtimeVersion, toVersion);
9415
+ return order === -1 || order === 0 && !launcher.runnable;
9416
+ }
9417
+ function startDriftHeal(options) {
9418
+ if ((options.platform ?? process.platform) !== "darwin") return false;
9419
+ const env = options.env ?? process.env;
9420
+ if (env[DAEMON_DRIFT_HEAL_ENV] === "0") return false;
9421
+ const root = options.packageRoot === void 0 ? packageRoot() : options.packageRoot;
9422
+ if (!isInstalledPackage(root)) return false;
9423
+ const euid = options.euid ?? process.geteuid?.();
9424
+ if (euid === void 0 || euid === 0) return false;
9425
+ const paths = { env, homeDir: options.homeDir };
9426
+ if (daemonExplicitlyDisabled(paths)) return false;
9427
+ const launcher = (options.selectedLauncher ?? (() => selectedDaemonLauncher(paths)))();
9428
+ if (!launcher || launcher.ownerUid !== euid) return false;
9429
+ const toVersion = options.cliVersion === void 0 ? packageVersion() : options.cliVersion;
9430
+ if (!(toVersion && needsHeal(launcher, toVersion))) return false;
9431
+ if (!apiUrlsMatch(
9432
+ getSiteUrlForEnvironment(launcher.apiUrl),
9433
+ getSiteUrlForEnvironment(env.PRIM_API_URL)
9434
+ )) {
9435
+ return false;
9436
+ }
9437
+ const runsConfigRoot = options.launchAgentRunsConfigRoot ?? (() => launchAgentRunsConfigRoot(paths));
9438
+ if (!runsConfigRoot()) return false;
9439
+ const primEntry = options.primEntry === void 0 ? binFile("prim") : options.primEntry;
9440
+ if (!primEntry) return false;
9441
+ const attempt = {
9442
+ attemptedAt: (options.nowMs ?? Date.now)(),
9443
+ fromVersion: launcher.runtimeVersion,
9444
+ toVersion
9445
+ };
9446
+ const claimed = withFileLockSync(
9447
+ daemonDriftHealClaimPath(paths),
9448
+ () => claimAttempt(daemonDriftHealMarkerPath(paths), attempt),
9449
+ { timeoutMs: 0, sleep: claimHeld }
9450
+ );
9451
+ if (!claimed) return false;
9452
+ return kickDaemonEnsure({
9453
+ primEntry,
9454
+ nodeEntry: options.nodeEntry,
9455
+ spawnProcess: options.spawnProcess,
9456
+ latestBootstrap: false
9457
+ });
9458
+ }
9459
+ function healDaemonDrift(options = {}) {
9460
+ try {
9461
+ return startDriftHeal(options);
9462
+ } catch {
9463
+ return false;
9464
+ }
9465
+ }
9466
+
9467
+ // src/lib/background-flush.ts
9468
+ import { spawn as spawn3 } from "child_process";
9469
+ import { readFileSync as readFileSync9 } from "fs";
9470
+
9471
+ // src/daemon/health.ts
9472
+ import { chmodSync as chmodSync3, existsSync as existsSync11, mkdirSync as mkdirSync7, renameSync as renameSync4, writeFileSync as writeFileSync6 } from "fs";
9473
+ import { dirname as dirname6, join as join12 } from "path";
9474
+ var HEARTBEAT_FRESH_MS = 9e4;
9475
+ var DAEMON_HEALTH_PATH = join12(primConfigDirectory(), "daemon-health.json");
9476
+
9477
+ // src/lib/background-flush.ts
9478
+ function record2(value) {
9479
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
9480
+ }
9481
+ function daemonOwnsDrain(options = {}) {
9482
+ try {
9483
+ const expectedVersion = options.expectedVersion === void 0 ? packageVersion() : options.expectedVersion;
9484
+ if (!expectedVersion) return false;
9485
+ const health = record2(
9486
+ JSON.parse(readFileSync9(options.healthPath ?? DAEMON_HEALTH_PATH, "utf8"))
9487
+ );
9488
+ const heartbeat = record2(health?.heartbeat);
9489
+ const ingestion = record2(health?.ingestion);
9490
+ if (!(health && heartbeat && ingestion)) return false;
9491
+ if (health.schemaVersion !== 1 || health.version !== expectedVersion) return false;
9492
+ const daemonSiteUrl = health.siteUrl;
9493
+ if (typeof daemonSiteUrl !== "string" || envSlug(daemonSiteUrl) !== envSlug(options.siteUrl ?? getSiteUrl())) {
9494
+ return false;
9495
+ }
9496
+ if (health.needsReauth === true) return false;
9497
+ const pid = health.pid;
9498
+ if (typeof pid !== "number" || !(options.isAlive ?? processIsAlive)(pid)) return false;
9499
+ const lastSuccessAt = heartbeat.lastSuccessAt;
9500
+ const now = options.now ?? Date.now();
9501
+ return heartbeat.consecutiveFailures === 0 && typeof lastSuccessAt === "number" && lastSuccessAt <= now && now - lastSuccessAt < HEARTBEAT_FRESH_MS && ingestion.consecutiveFailures === 0;
9502
+ } catch {
9503
+ return false;
9504
+ }
9505
+ }
9506
+ function startBackgroundFlush(options = {}) {
9507
+ try {
9508
+ if ((options.daemonOwnsDrain ?? daemonOwnsDrain)()) return false;
9509
+ if (!(options.needsFlush ?? journalNeedsFlush)()) return false;
9510
+ const primEntry = options.primEntry === void 0 ? binFile("prim") : options.primEntry;
9511
+ if (!primEntry) return false;
9512
+ const child = (options.spawnProcess ?? spawn3)(
9513
+ options.nodeEntry ?? process.execPath,
9514
+ [primEntry, "moves", "flush"],
9515
+ // Ignored stdio matters as much as detaching: a child holding inherited
9516
+ // pipes would keep a caller that captures them (spawnSync) waiting.
9517
+ // windowsHide keeps the detached child from opening a console window.
9518
+ // The child is unattended even when its parent is not: `doctor` or
9519
+ // `statusline` must not reach the daemon drift heal through its drain.
9520
+ { detached: true, stdio: "ignore", windowsHide: true, env: unattendedEnv() }
9521
+ );
9522
+ child.once("error", () => {
9523
+ });
9524
+ child.unref();
9525
+ return true;
9526
+ } catch {
9527
+ return false;
9528
+ }
9529
+ }
9530
+
9010
9531
  // src/lib/startup-background.ts
9011
9532
  var ROOT_OPTIONS = /* @__PURE__ */ new Set(["-y", "--yes", "--non-interactive"]);
9533
+ var HELP_OPTIONS = /* @__PURE__ */ new Set(["-h", "--help"]);
9534
+ var DAEMON_HEAL_COMMANDS = /* @__PURE__ */ new Map([
9535
+ // Credentials; `auth login` is how a person returns to a daemon held for re-auth.
9536
+ ["auth", /* @__PURE__ */ new Set(["api-keys", "login", "set-token", "status"])],
9537
+ // Reading and curating the decision graph: the everyday attended surface.
9538
+ [
9539
+ "decisions",
9540
+ /* @__PURE__ */ new Set([
9541
+ "cascade",
9542
+ "check",
9543
+ "confirm",
9544
+ "create",
9545
+ "demote",
9546
+ "link",
9547
+ "promote",
9548
+ "publish",
9549
+ "ratify",
9550
+ "recent",
9551
+ "repairs",
9552
+ "rescope",
9553
+ "restore",
9554
+ "show",
9555
+ "supersede",
9556
+ "unlink",
9557
+ "withdraw"
9558
+ ])
9559
+ ],
9560
+ ["reconcile", "any"],
9561
+ // Repository connection, activation, and journal plumbing a person steers.
9562
+ // Hook-side `moves flush` drains are unattended and never reach this table.
9563
+ ["github", /* @__PURE__ */ new Set(["connect"])],
9564
+ ["enable", "any"],
9565
+ ["moves", /* @__PURE__ */ new Set(["bind", "drop", "flush", "status", "tail"])],
9566
+ ["session", /* @__PURE__ */ new Set(["drop", "list", "start"])],
9567
+ // Installing or inspecting an integration, never removing one.
9568
+ ["claude", /* @__PURE__ */ new Set(["install", "preauth", "status"])],
9569
+ ["codex", /* @__PURE__ */ new Set(["install", "status"])],
9570
+ ["cursor", /* @__PURE__ */ new Set(["install", "status"])],
9571
+ ["hermes", /* @__PURE__ */ new Set(["install", "status"])],
9572
+ ["hooks", /* @__PURE__ */ new Set(["install"])],
9573
+ ["skill", /* @__PURE__ */ new Set(["install", "status"])],
9574
+ ["welcome", "any"]
9575
+ ]);
9012
9576
  function commandArguments(argv) {
9013
9577
  let index = 0;
9014
9578
  while (ROOT_OPTIONS.has(argv[index] ?? "")) index += 1;
9015
9579
  return argv.slice(index);
9016
9580
  }
9581
+ function mayHealDaemon(argv, env) {
9582
+ if (isUnattended(env) || env[SETUP_ORCHESTRATOR_ENV] === "1") return false;
9583
+ const args = commandArguments(argv);
9584
+ if (args.some((arg) => HELP_OPTIONS.has(arg))) return false;
9585
+ const [command, subcommand] = args;
9586
+ const allowed = command === void 0 ? void 0 : DAEMON_HEAL_COMMANDS.get(command);
9587
+ if (allowed === void 0) return false;
9588
+ return allowed === "any" || subcommand !== void 0 && allowed.has(subcommand);
9589
+ }
9017
9590
  function runStartupBackgroundWork(argv, env, work) {
9018
- const [command, subcommand] = commandArguments(argv);
9591
+ const args = commandArguments(argv);
9592
+ const [command, subcommand] = args;
9019
9593
  const uninstall = command === "uninstall" || env[UNINSTALL_ORCHESTRATOR_ENV] === "1";
9020
9594
  if (uninstall) return;
9021
9595
  work.notify();
9022
- if (command !== "moves" || subcommand !== "flush") work.flush().catch(() => {
9023
- });
9596
+ const setupDaemonDrains = command === "setup" && !args.includes("--no-daemon") || env[SETUP_DAEMON_DRAINS_ENV] === "1";
9597
+ if (!setupDaemonDrains && (command !== "moves" || subcommand !== "flush")) work.flush();
9598
+ if (mayHealDaemon(argv, env)) work.healDaemon();
9024
9599
  }
9025
9600
 
9026
9601
  // src/index.ts
9027
- var __dirname = dirname6(fileURLToPath2(import.meta.url));
9028
- var pkg = JSON.parse(readFileSync9(resolve4(__dirname, "../package.json"), "utf-8"));
9602
+ var __dirname = dirname7(fileURLToPath2(import.meta.url));
9603
+ var pkg = JSON.parse(readFileSync10(resolve4(__dirname, "../package.json"), "utf-8"));
9029
9604
  runStartupBackgroundWork(process.argv.slice(2), process.env, {
9030
9605
  notify: () => updateNotifier({ pkg }).notify(),
9031
- flush: () => flushIfNeeded()
9606
+ flush: () => startBackgroundFlush(),
9607
+ healDaemon: () => healDaemonDrift()
9032
9608
  });
9033
9609
  var program = new Command();
9034
9610
  program.name("prim").description("CLI for Primitive's decision graph").version(pkg.version).option("-y, --yes", "auto-confirm prompts").option(