@indigoai-us/hq-cli 5.345.155 → 5.345.159

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 (169) hide show
  1. package/CHANGELOG.md +289 -0
  2. package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +110 -2
  3. package/assets/scaffold/core/scripts/rebuild-projects-index.sh +101 -5
  4. package/assets/scaffold/core/scripts/worktree.sh +4 -0
  5. package/dist/command-catalog.generated.d.ts +536 -7
  6. package/dist/command-catalog.generated.js +676 -24
  7. package/dist/command-registration-plan.d.ts +18 -0
  8. package/dist/command-registration-plan.js +4 -0
  9. package/dist/commands/_patterns.d.ts +17 -0
  10. package/dist/commands/_patterns.js +22 -0
  11. package/dist/commands/agent-inbox.js +2 -2
  12. package/dist/commands/agent-kit.js +8 -6
  13. package/dist/commands/bot.js +7 -4
  14. package/dist/commands/channel-suggest.d.ts +56 -0
  15. package/dist/commands/channel-suggest.js +170 -0
  16. package/dist/commands/channels.js +7 -1
  17. package/dist/commands/cloud.d.ts +45 -0
  18. package/dist/commands/cloud.js +150 -10
  19. package/dist/commands/company.js +4 -3
  20. package/dist/commands/core-checkpoint-native.d.ts +18 -0
  21. package/dist/commands/core-checkpoint-native.js +67 -2
  22. package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
  23. package/dist/commands/core-pipeline-driver-native.js +131 -5
  24. package/dist/commands/core.js +13 -1
  25. package/dist/commands/crm.js +83 -6
  26. package/dist/commands/deploy-access.d.ts +33 -0
  27. package/dist/commands/deploy-access.js +322 -0
  28. package/dist/commands/dm-attach.d.ts +106 -0
  29. package/dist/commands/dm-attach.js +184 -0
  30. package/dist/commands/dm.d.ts +37 -0
  31. package/dist/commands/dm.js +172 -10
  32. package/dist/commands/feedback-show.d.ts +40 -0
  33. package/dist/commands/feedback-show.js +261 -0
  34. package/dist/commands/feedback.d.ts +21 -0
  35. package/dist/commands/feedback.js +152 -2
  36. package/dist/commands/files-browse.d.ts +16 -0
  37. package/dist/commands/files-browse.js +52 -1
  38. package/dist/commands/files.d.ts +24 -0
  39. package/dist/commands/files.js +52 -1
  40. package/dist/commands/integrations-connect.js +7 -3
  41. package/dist/commands/integrations-core.d.ts +13 -1
  42. package/dist/commands/integrations-core.js +49 -4
  43. package/dist/commands/integrations.js +4 -2
  44. package/dist/commands/lanes-accounts.js +16 -1
  45. package/dist/commands/lanes-loop.js +14 -4
  46. package/dist/commands/lanes-message.js +6 -4
  47. package/dist/commands/lanes-resume.js +36 -13
  48. package/dist/commands/lanes-shared.d.ts +5 -0
  49. package/dist/commands/lanes-shared.js +13 -3
  50. package/dist/commands/lanes-watch.js +1 -0
  51. package/dist/commands/lanes.d.ts +20 -1
  52. package/dist/commands/lanes.js +201 -51
  53. package/dist/commands/mesh.js +149 -1
  54. package/dist/commands/monitor.d.ts +1 -0
  55. package/dist/commands/monitor.js +4 -1
  56. package/dist/commands/repo.d.ts +41 -0
  57. package/dist/commands/repo.js +331 -0
  58. package/dist/commands/scan.d.ts +3 -1
  59. package/dist/commands/scan.js +7 -3
  60. package/dist/commands/secrets.d.ts +37 -0
  61. package/dist/commands/secrets.js +185 -16
  62. package/dist/commands/signals.js +17 -5
  63. package/dist/commands/staff/listings.d.ts +41 -0
  64. package/dist/commands/staff/listings.js +160 -0
  65. package/dist/commands/staff/reset-test-account.d.ts +3 -2
  66. package/dist/commands/staff/reset-test-account.js +4 -3
  67. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2629 -1036
  68. package/dist/hq-cloud-sync-runner/package.json +1 -1
  69. package/dist/lib/agent-kit/inbox-state.d.ts +7 -0
  70. package/dist/lib/agent-kit/inbox-state.js +5 -1
  71. package/dist/lib/agent-kit/mcp/tools.js +2 -1
  72. package/dist/lib/agent-kit/run/inbox.js +2 -1
  73. package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
  74. package/dist/lib/agent-kit/wake-secrets.js +32 -0
  75. package/dist/lib/agent-kit/wake.d.ts +4 -0
  76. package/dist/lib/agent-kit/wake.js +49 -3
  77. package/dist/lib/bot/inflight.d.ts +7 -1
  78. package/dist/lib/bot/inflight.js +10 -2
  79. package/dist/lib/bot/prompt.js +3 -0
  80. package/dist/lib/bot/run.js +72 -20
  81. package/dist/lib/bot/session.d.ts +14 -0
  82. package/dist/lib/bot/session.js +70 -0
  83. package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
  84. package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
  85. package/dist/lib/capacity/lane-login.d.ts +6 -0
  86. package/dist/lib/capacity/lane-login.js +5 -0
  87. package/dist/lib/core-utils/worktree.js +2 -0
  88. package/dist/lib/daemon/exit-cause.d.ts +43 -0
  89. package/dist/lib/daemon/exit-cause.js +88 -0
  90. package/dist/lib/daemon/handoff.d.ts +2 -0
  91. package/dist/lib/daemon/handoff.js +6 -1
  92. package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
  93. package/dist/lib/daemon/paths.d.ts +2 -0
  94. package/dist/lib/daemon/paths.js +1 -0
  95. package/dist/lib/daemon/run.js +13 -0
  96. package/dist/lib/daemon/supervisor.d.ts +54 -1
  97. package/dist/lib/daemon/supervisor.js +297 -21
  98. package/dist/lib/daemon/unit-holds.d.ts +22 -0
  99. package/dist/lib/daemon/unit-holds.js +86 -0
  100. package/dist/lib/doctor/checks/sync-health.d.ts +9 -0
  101. package/dist/lib/doctor/checks/sync-health.js +66 -1
  102. package/dist/lib/doctor/company-resolution-state.d.ts +30 -0
  103. package/dist/lib/doctor/company-resolution-state.js +89 -0
  104. package/dist/lib/doctor/fix/apply.d.ts +36 -6
  105. package/dist/lib/doctor/fix/apply.js +128 -12
  106. package/dist/lib/doctor/types.d.ts +1 -1
  107. package/dist/lib/doctor/types.js +1 -0
  108. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  109. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  110. package/dist/lib/hook-engine/resolve.js +6 -2
  111. package/dist/lib/index-render/company-knowledge.d.ts +1 -1
  112. package/dist/lib/index-render/company-knowledge.js +4 -5
  113. package/dist/lib/index-render/index.js +44 -3
  114. package/dist/lib/index-render/projects.d.ts +1 -1
  115. package/dist/lib/index-render/projects.js +5 -5
  116. package/dist/lib/lanes/account-company.d.ts +28 -0
  117. package/dist/lib/lanes/account-company.js +53 -0
  118. package/dist/lib/lanes/accounts.d.ts +22 -3
  119. package/dist/lib/lanes/accounts.js +38 -2
  120. package/dist/lib/lanes/claude-balance.d.ts +6 -1
  121. package/dist/lib/lanes/claude-balance.js +6 -1
  122. package/dist/lib/lanes/claude-credential.d.ts +78 -0
  123. package/dist/lib/lanes/claude-credential.js +163 -0
  124. package/dist/lib/lanes/compaction.d.ts +2 -0
  125. package/dist/lib/lanes/compaction.js +9 -0
  126. package/dist/lib/lanes/config.js +6 -0
  127. package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
  128. package/dist/lib/lanes/hooks-install.js +2 -1
  129. package/dist/lib/lanes/host-capacity.d.ts +2 -0
  130. package/dist/lib/lanes/host-capacity.js +1 -0
  131. package/dist/lib/lanes/keep-alive.d.ts +1 -1
  132. package/dist/lib/lanes/keep-alive.js +3 -1
  133. package/dist/lib/lanes/links.js +10 -4
  134. package/dist/lib/lanes/loop.js +16 -1
  135. package/dist/lib/lanes/node-binary.d.ts +17 -0
  136. package/dist/lib/lanes/node-binary.js +30 -0
  137. package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
  138. package/dist/lib/lanes/outpost-dispatch.js +1 -0
  139. package/dist/lib/lanes/reconcile.js +4 -0
  140. package/dist/lib/lanes/remote-control-lane.js +2 -0
  141. package/dist/lib/lanes/spawn.d.ts +3 -0
  142. package/dist/lib/lanes/spawn.js +119 -9
  143. package/dist/lib/lanes/store.d.ts +6 -0
  144. package/dist/lib/lanes/store.js +107 -18
  145. package/dist/lib/lanes/types.d.ts +2 -0
  146. package/dist/lib/lanes/watch.js +28 -5
  147. package/dist/lib/lanes/watcher-main.js +4 -3
  148. package/dist/lib/lanes/watcher.d.ts +4 -0
  149. package/dist/lib/lanes/watcher.js +110 -21
  150. package/dist/lib/mesh/api.d.ts +32 -0
  151. package/dist/lib/mesh/api.js +75 -0
  152. package/dist/lib/secret-scan/detect.js +9 -0
  153. package/dist/lib/secret-scan/repo-config.d.ts +41 -0
  154. package/dist/lib/secret-scan/repo-config.js +237 -0
  155. package/dist/lib/secret-scan/rules.d.ts +7 -1
  156. package/dist/lib/secret-scan/rules.js +3 -1
  157. package/dist/lib/secret-scan/staged.d.ts +5 -0
  158. package/dist/lib/secret-scan/staged.js +19 -2
  159. package/dist/lib/secret-scan/types.d.ts +2 -0
  160. package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
  161. package/dist/lib/sync/server-owned-local-only.js +75 -0
  162. package/dist/sentry-before-send.js +1 -0
  163. package/dist/utils/access-denied-hint.js +6 -0
  164. package/dist/utils/client-health.d.ts +34 -0
  165. package/dist/utils/client-health.js +96 -3
  166. package/dist/utils/package-use-lease.js +27 -9
  167. package/dist/utils/version-gate.d.ts +7 -0
  168. package/dist/utils/version-gate.js +66 -0
  169. package/package.json +8 -8
@@ -825,7 +825,7 @@ async function settleManagerStatusGate(hqRoot, lane, manifestTurnId, at, deps, l
825
825
  written = false;
826
826
  }
827
827
  if (written) {
828
- return retryUpdateLane(hqRoot, lane.lane_id, "watcher", (current) => {
828
+ return updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => {
829
829
  if (current.status_gate?.turn_id !== gate.turn_id || current.status_gate.consecutive_misses === 0)
830
830
  return current;
831
831
  return { ...current, status_gate: { ...current.status_gate, consecutive_misses: 0 } };
@@ -837,7 +837,7 @@ async function settleManagerStatusGate(hqRoot, lane, manifestTurnId, at, deps, l
837
837
  path: gate.status_path,
838
838
  at,
839
839
  identified_by: target.identified_by ?? "juniors",
840
- }, resumeText);
840
+ }, resumeText, (current) => followupRunMoved(hqRoot, lane.lane_id, deps, current));
841
841
  const miss = updated.status_gate_history?.find((entry) => entry.event === "status_missed" && entry.turn_id === gate.turn_id);
842
842
  if (miss) {
843
843
  logDecision({
@@ -876,7 +876,7 @@ async function settleBlockerFreshness(hqRoot, lane, manifestTurnId, at, deps, me
876
876
  if (resolution.stale.length === 0) {
877
877
  if (lane.blocker_freshness_state === undefined)
878
878
  return lane;
879
- return retryUpdateLane(hqRoot, lane.lane_id, "watcher", (current) => current.blocker_freshness_state === undefined
879
+ return updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => current.blocker_freshness_state === undefined
880
880
  ? current
881
881
  : { ...current, blocker_freshness_state: undefined });
882
882
  }
@@ -895,7 +895,7 @@ async function settleBlockerFreshness(hqRoot, lane, manifestTurnId, at, deps, me
895
895
  ? findBlockerFreshnessSessionInbox(hqRoot, lane.senior.id)
896
896
  : { reason: "The session senior has no session id" }
897
897
  : undefined;
898
- let updated = retryUpdateLane(hqRoot, lane.lane_id, "watcher", (current) => {
898
+ let updated = updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => {
899
899
  deliverEscalation = false;
900
900
  deliverFallback = false;
901
901
  retryUndeliverable = false;
@@ -1014,7 +1014,7 @@ async function settleBlockerFreshness(hqRoot, lane, manifestTurnId, at, deps, me
1014
1014
  kind: "blocker_stale_escalation_undeliverable",
1015
1015
  text: `${escalationText} The session senior inbox is unavailable (${reason}); this escalation is recorded in the lane inbox for its next turn.`,
1016
1016
  });
1017
- updated = retryUpdateLane(hqRoot, lane.lane_id, "watcher", (current) => {
1017
+ updated = updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => {
1018
1018
  const state = current.blocker_freshness_state;
1019
1019
  if (state === undefined)
1020
1020
  return current;
@@ -1037,7 +1037,7 @@ async function settleBlockerFreshness(hqRoot, lane, manifestTurnId, at, deps, me
1037
1037
  const message = error instanceof Error ? error.message : String(error);
1038
1038
  process.stderr.write(`hq lanes watcher: blocker freshness fallback delivery failed for ${lane.lane_id}: ${message}\n`);
1039
1039
  logDecision({ event: "blocker_stale_escalation_fallback_failed", at, turn_id: turnId, fingerprint, error: message });
1040
- updated = retryUpdateLane(hqRoot, lane.lane_id, "watcher", (current) => {
1040
+ updated = updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => {
1041
1041
  const state = current.blocker_freshness_state;
1042
1042
  const failureReason = `${undeliverableReason ?? `Session ${lane.senior.id ?? "unknown"} has no monitor-session inbox`}; lane inbox fallback failed: ${message}`;
1043
1043
  return {
@@ -1080,7 +1080,7 @@ async function settleBlockerFreshness(hqRoot, lane, manifestTurnId, at, deps, me
1080
1080
  }
1081
1081
  logDecision({ event: "blocker_stale_escalation_delivered", at, turn_id: turnId, senior_kind: lane.senior.kind });
1082
1082
  if (retryUndeliverable) {
1083
- updated = retryUpdateLane(hqRoot, lane.lane_id, "watcher", (current) => {
1083
+ updated = updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => {
1084
1084
  const state = current.blocker_freshness_state;
1085
1085
  const failedFallbackStillOpen = (current.status_gate_history ?? []).some((entry) => entry.event === "blocker_stale_escalation_undeliverable" &&
1086
1086
  entry.fingerprint === fingerprint && entry.delivery_failed === true &&
@@ -1104,7 +1104,7 @@ async function settleBlockerFreshness(hqRoot, lane, manifestTurnId, at, deps, me
1104
1104
  const message = error instanceof Error ? error.message : String(error);
1105
1105
  process.stderr.write(`hq lanes watcher: blocker freshness escalation delivery failed for ${lane.lane_id}: ${message}\n`);
1106
1106
  logDecision({ event: "blocker_stale_escalation_delivery_failed", at, turn_id: turnId, error: message });
1107
- updated = retryUpdateLane(hqRoot, lane.lane_id, "watcher", (current) => {
1107
+ updated = updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => {
1108
1108
  const state = current.blocker_freshness_state;
1109
1109
  if (state?.escalated_fingerprint !== fingerprint)
1110
1110
  return current;
@@ -1151,7 +1151,7 @@ async function settleKrFreshness(hqRoot, lane, manifestTurnId, at, deps, logDeci
1151
1151
  if (stalled.length === 0) {
1152
1152
  if (lane.kr_freshness_state === undefined)
1153
1153
  return lane;
1154
- return retryUpdateLane(hqRoot, lane.lane_id, "watcher", (current) => current.kr_freshness_state === undefined
1154
+ return updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => current.kr_freshness_state === undefined
1155
1155
  ? current
1156
1156
  : { ...current, kr_freshness_state: undefined });
1157
1157
  }
@@ -1159,7 +1159,7 @@ async function settleKrFreshness(hqRoot, lane, manifestTurnId, at, deps, logDeci
1159
1159
  const names = stalled.map((kpi) => kpi.name);
1160
1160
  const turnId = manifestTurnId ?? lane.status_gate?.turn_id ?? `unknown-${at}`;
1161
1161
  const resumeText = `These KPIs in ${resolution.statusPath} were not re-measured in the last ${resolution.windowHours}h: ${names.join(", ")}. Measure each one now, update value and measured_at, then end the turn.`;
1162
- return retryUpdateLane(hqRoot, lane.lane_id, "watcher", (current) => {
1162
+ return updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => {
1163
1163
  const history = current.status_gate_history ?? [];
1164
1164
  if (history.some((entry) => entry.event === "kr_stalled" && entry.turn_id === turnId))
1165
1165
  return current;
@@ -1297,6 +1297,38 @@ function defaultSleep(ms) {
1297
1297
  });
1298
1298
  }
1299
1299
  const WATCHER_LOOP_OWNER_FILE = "watcher-loop.pid";
1300
+ /** Run identity (spawn manifest envelope path) of the current claimant. */
1301
+ const WATCHER_LOOP_RUN_FILE = "watcher-loop.run";
1302
+ /** How long a new phase watcher waits for an older phase's watcher to let go. */
1303
+ export const WATCHER_CLAIM_WAIT_MS = 60_000;
1304
+ const WATCHER_CLAIM_RETRY_MS = 250;
1305
+ /** Identifies one worker run of a lane; changes on every resume. */
1306
+ function watcherRunId(runDir) {
1307
+ try {
1308
+ return readSpawnManifest(runDir).envelope_path ?? "";
1309
+ }
1310
+ catch {
1311
+ return "";
1312
+ }
1313
+ }
1314
+ /** True when the lane has moved on to a run this watcher does not own. */
1315
+ function followupRunMoved(hqRoot, laneId, deps, current) {
1316
+ if (deps.followupRun === undefined)
1317
+ return false;
1318
+ return current.state === "running" || watcherRunId(laneRunDir(hqRoot, laneId)) !== deps.followupRun;
1319
+ }
1320
+ /** Watcher lane write that is a no-op once the lane belongs to a newer run. */
1321
+ function updateLaneFollowup(hqRoot, laneId, deps, updater) {
1322
+ return retryUpdateLane(hqRoot, laneId, "watcher", (current) => followupRunMoved(hqRoot, laneId, deps, current) ? current : updater(current));
1323
+ }
1324
+ function readClaimRunId(runDir) {
1325
+ try {
1326
+ return fs.readFileSync(path.join(runDir, WATCHER_LOOP_RUN_FILE), "utf8").trim();
1327
+ }
1328
+ catch {
1329
+ return null;
1330
+ }
1331
+ }
1300
1332
  function claimWatcherLoop(hqRoot, laneId, deps) {
1301
1333
  const runDir = laneRunDir(hqRoot, laneId);
1302
1334
  const ownerPath = path.join(runDir, WATCHER_LOOP_OWNER_FILE);
@@ -1310,6 +1342,7 @@ function claimWatcherLoop(hqRoot, laneId, deps) {
1310
1342
  }
1311
1343
  if (existing !== null)
1312
1344
  fs.unlinkSync(ownerPath);
1345
+ fs.writeFileSync(path.join(runDir, WATCHER_LOOP_RUN_FILE), `${watcherRunId(runDir)}\n`, "utf8");
1313
1346
  writePidRecord(ownerPath, {
1314
1347
  pid: process.pid,
1315
1348
  starttime: readStarttime(process.pid) ?? readProcStarttime(process.pid) ?? undefined,
@@ -1319,7 +1352,11 @@ function claimWatcherLoop(hqRoot, laneId, deps) {
1319
1352
  });
1320
1353
  if (owner === null)
1321
1354
  return null;
1355
+ let released = false;
1322
1356
  return () => {
1357
+ if (released)
1358
+ return;
1359
+ released = true;
1323
1360
  withLaneHireLocks(hqRoot, [laneId], () => {
1324
1361
  try {
1325
1362
  const current = fs.statSync(ownerPath);
@@ -1391,7 +1428,7 @@ async function maybeContinueAfterMissingEnvelopeRepair(hqRoot, lane, envelope, e
1391
1428
  }
1392
1429
  if (!enabled)
1393
1430
  return lane;
1394
- const queued = retryUpdateLane(hqRoot, lane.lane_id, "watcher", (current) => {
1431
+ const queued = updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => {
1395
1432
  if (current.version !== lane.version ||
1396
1433
  (current.state !== "done" && current.state !== "blocked") ||
1397
1434
  current.last_envelope?.ref !== envelopeRef ||
@@ -1421,17 +1458,59 @@ async function maybeContinueAfterMissingEnvelopeRepair(hqRoot, lane, envelope, e
1421
1458
  }
1422
1459
  /** Detached and synchronous classifiers share one process-identity claim. */
1423
1460
  export async function runWatcherLoop(hqRoot, laneId, deps = {}) {
1424
- const release = claimWatcherLoop(hqRoot, laneId, deps);
1425
- if (release === null)
1426
- return;
1461
+ let release = claimWatcherLoop(hqRoot, laneId, deps);
1462
+ if (release === null) {
1463
+ // A live claimant from an older run (e.g. the previous phase's watcher
1464
+ // still finishing its post-terminal mesh call) lets go soon. Wait for it
1465
+ // instead of leaving this run unwatched. A claimant on the same run is a
1466
+ // true duplicate and this watcher exits at once.
1467
+ const sleep = deps.sleep ?? defaultSleep;
1468
+ const now = deps.now ?? (() => Date.now());
1469
+ const runDir = laneRunDir(hqRoot, laneId);
1470
+ const myRun = watcherRunId(runDir);
1471
+ const waitMs = deps.claimWaitMs ?? WATCHER_CLAIM_WAIT_MS;
1472
+ const started = now();
1473
+ let claimantRun = readClaimRunId(runDir);
1474
+ while (release === null && claimantRun !== myRun && now() - started < waitMs) {
1475
+ await sleep(WATCHER_CLAIM_RETRY_MS);
1476
+ release = claimWatcherLoop(hqRoot, laneId, deps);
1477
+ if (release === null)
1478
+ claimantRun = readClaimRunId(runDir);
1479
+ }
1480
+ if (release === null) {
1481
+ let claimantPid = null;
1482
+ try {
1483
+ claimantPid = readPidRecord(path.join(runDir, WATCHER_LOOP_OWNER_FILE))?.pid ?? null;
1484
+ }
1485
+ catch {
1486
+ claimantPid = null;
1487
+ }
1488
+ const record = {
1489
+ event: "watcher_claim_lost",
1490
+ at: new Date(now()).toISOString(),
1491
+ claimant_pid: claimantPid,
1492
+ claimant_run: claimantRun,
1493
+ run: myRun,
1494
+ waited_ms: now() - started,
1495
+ };
1496
+ try {
1497
+ writeWatcherDecision(deps.watcherOutPath ?? watcherOutLogPath(hqRoot, laneId), record, deps.watcherOutMaxBytes ?? WATCHER_OUT_MAX_BYTES);
1498
+ }
1499
+ catch {
1500
+ // Logging is best effort; stderr below still records the loss.
1501
+ }
1502
+ process.stderr.write(`hq lanes watcher: claim lost for ${laneId} to pid ${claimantPid ?? "unknown"}\n`);
1503
+ return;
1504
+ }
1505
+ }
1427
1506
  try {
1428
- await runWatcherLoopOwned(hqRoot, laneId, deps);
1507
+ await runWatcherLoopOwned(hqRoot, laneId, deps, release);
1429
1508
  }
1430
1509
  finally {
1431
1510
  release();
1432
1511
  }
1433
1512
  }
1434
- async function runWatcherLoopOwned(hqRoot, laneId, deps) {
1513
+ async function runWatcherLoopOwned(hqRoot, laneId, deps, releaseClaim = () => { }) {
1435
1514
  const sleep = deps.sleep ?? defaultSleep;
1436
1515
  const now = deps.now ?? (() => Date.now());
1437
1516
  const isAlive = deps.isAlive ?? isPidAlive;
@@ -1459,6 +1538,7 @@ async function runWatcherLoopOwned(hqRoot, laneId, deps) {
1459
1538
  const mesh = deps.mesh === undefined ? createLaneMesh({ interactive: false }) : deps.mesh;
1460
1539
  const storyStatusSuppressions = new Set();
1461
1540
  const meshRequestTimeoutMs = deps.meshRequestTimeoutMs ?? 10_000;
1541
+ const ownRun = watcherRunId(runDir);
1462
1542
  const initialLane = readLaneSoft(hqRoot, laneId);
1463
1543
  if (mesh && initialLane) {
1464
1544
  // Every armed watcher refreshes the server descriptor. This migrates old
@@ -1889,6 +1969,12 @@ async function runWatcherLoopOwned(hqRoot, laneId, deps) {
1889
1969
  state: updated.state,
1890
1970
  version: updated.version,
1891
1971
  });
1972
+ // The lane is classified and this watcher stops polling after this
1973
+ // tick. Let go of the single-owner claim before the slow mesh and
1974
+ // follow-up awaits, so a watcher armed for the next phase can claim.
1975
+ if (tick.done)
1976
+ releaseClaim();
1977
+ const followupDeps = tick.done ? { ...deps, followupRun: ownRun } : deps;
1892
1978
  // Spec §8 terminal rows: done → lane_done + story review; blocked →
1893
1979
  // lane_blocked; ask → lane_ask with the queue ids. After the state
1894
1980
  // write, so a mesh failure can never leave the lane unclassified.
@@ -1914,9 +2000,9 @@ async function runWatcherLoopOwned(hqRoot, laneId, deps) {
1914
2000
  }
1915
2001
  }
1916
2002
  if (terminalWrite.changed) {
1917
- updated = await settleManagerStatusGate(hqRoot, updated, manifest.turn_id, tick.emit.at, deps, logDecision);
1918
- updated = await settleBlockerFreshness(hqRoot, updated, manifest.turn_id, tick.emit.at, deps, mesh, logDecision);
1919
- updated = await settleKrFreshness(hqRoot, updated, manifest.turn_id, tick.emit.at, deps, logDecision);
2003
+ updated = await settleManagerStatusGate(hqRoot, updated, manifest.turn_id, tick.emit.at, followupDeps, logDecision);
2004
+ updated = await settleBlockerFreshness(hqRoot, updated, manifest.turn_id, tick.emit.at, followupDeps, mesh, logDecision);
2005
+ updated = await settleKrFreshness(hqRoot, updated, manifest.turn_id, tick.emit.at, followupDeps, logDecision);
1920
2006
  }
1921
2007
  const managerStatusFollowupQueued = manifest.turn_id !== undefined &&
1922
2008
  updated.status_gate_history?.some((entry) => "turn_id" in entry && entry.turn_id === manifest.turn_id &&
@@ -1924,11 +2010,14 @@ async function runWatcherLoopOwned(hqRoot, laneId, deps) {
1924
2010
  (entry.event === "blocker_stale" && entry.resume_queued === true) ||
1925
2011
  (entry.event === "kr_stalled" && entry.resume_queued === true))) === true;
1926
2012
  if (terminalWrite.changed && !managerStatusFollowupQueued) {
1927
- updated = await maybeContinueAfterMissingEnvelopeRepair(hqRoot, updated, effectiveEnvelope ?? null, latest?.ref, tick.emit.at, deps, logDecision);
2013
+ updated = await maybeContinueAfterMissingEnvelopeRepair(hqRoot, updated, effectiveEnvelope ?? null, latest?.ref, tick.emit.at, followupDeps, logDecision);
1928
2014
  }
1929
2015
  if (terminalWrite.changed && deps.recoverQueuedMessages && !managerStatusFollowupQueued && updated.state !== "queued") {
1930
2016
  try {
1931
- if (dropboxInboxHasMessages(hqRoot, laneId)) {
2017
+ if (followupRunMoved(hqRoot, laneId, followupDeps, readLane(hqRoot, laneId))) {
2018
+ logDecision({ event: "message_recovery_skipped", at: tick.emit.at, reason: "newer_run" });
2019
+ }
2020
+ else if (dropboxInboxHasMessages(hqRoot, laneId)) {
1932
2021
  await deps.recoverQueuedMessages(updated);
1933
2022
  logDecision({
1934
2023
  event: "message_recovery",
@@ -176,6 +176,38 @@ export declare function putProjectView(token: string, companyUid: string, projec
176
176
  }): Promise<MeshProjectView>;
177
177
  /** Register the project genesis thread and deterministic project channel. */
178
178
  export declare function registerMeshProject(token: string, companyUid: string, projectId: string, channelId?: string): Promise<MeshProjectRegistration>;
179
+ export declare const PROJECT_LIFECYCLE_STATUSES: readonly ["active", "completed", "archived"];
180
+ export type MeshProjectLifecycleStatus = (typeof PROJECT_LIFECYCLE_STATUSES)[number];
181
+ /** One row of the server's PROJECT_LIFECYCLE record for a project. */
182
+ export interface MeshProjectLifecycle {
183
+ projectId: string;
184
+ status: MeshProjectLifecycleStatus;
185
+ /** OCC version of the lifecycle row (not the Board version). */
186
+ version: number;
187
+ updatedAt: string;
188
+ updatedBy: string;
189
+ }
190
+ /**
191
+ * List every lifecycle row for a company (follows the 25-row server pages).
192
+ * A project with no row has never left `active` and is not listed.
193
+ */
194
+ export declare function listProjectLifecycles(token: string, companyUid: string): Promise<MeshProjectLifecycle[]>;
195
+ /** The lifecycle row for one project, or null when it has never been changed. */
196
+ export declare function getProjectLifecycle(token: string, companyUid: string, projectId: string): Promise<MeshProjectLifecycle | null>;
197
+ /**
198
+ * PATCH /v1/work-mesh/projects/{id}/lifecycle. `expectedVersion` is the
199
+ * current lifecycle row version (0 when the project has no row yet). The
200
+ * server answers 409 BOARD_NONEMPTY when archiving a Board that still has
201
+ * stories unless `confirmNonempty` is set, and a plain 409 on a version
202
+ * mismatch. Nothing is deleted: stories, folder, channel, members and
203
+ * history stay; the project only leaves the active list and the organize
204
+ * candidates.
205
+ */
206
+ export declare function setProjectLifecycle(token: string, companyUid: string, projectId: string, change: {
207
+ status: MeshProjectLifecycleStatus;
208
+ expectedVersion: number;
209
+ confirmNonempty?: boolean;
210
+ }): Promise<MeshProjectLifecycle>;
179
211
  export declare function pairDmUidsFromInbox(inbox: unknown): string[];
180
212
  export interface WarmCacheResult {
181
213
  directory: boolean;
@@ -515,6 +515,81 @@ export async function registerMeshProject(token, companyUid, projectId, channelI
515
515
  channelCreated: result?.channelCreated === true,
516
516
  };
517
517
  }
518
+ // ---------------------------------------------------------------------------
519
+ // Project lifecycle (active | completed | archived)
520
+ // ---------------------------------------------------------------------------
521
+ export const PROJECT_LIFECYCLE_STATUSES = ["active", "completed", "archived"];
522
+ function lifecycleFromRow(row) {
523
+ const rec = asRecord(row);
524
+ const projectId = String(rec?.projectId || "").trim();
525
+ const status = String(rec?.status || "").trim();
526
+ const version = Number(rec?.version);
527
+ if (!projectId || !Number.isSafeInteger(version) || version < 0)
528
+ return null;
529
+ if (!PROJECT_LIFECYCLE_STATUSES.includes(status))
530
+ return null;
531
+ return {
532
+ projectId,
533
+ status: status,
534
+ version,
535
+ updatedAt: String(rec?.updatedAt || ""),
536
+ updatedBy: String(rec?.updatedBy || ""),
537
+ };
538
+ }
539
+ /**
540
+ * List every lifecycle row for a company (follows the 25-row server pages).
541
+ * A project with no row has never left `active` and is not listed.
542
+ */
543
+ export async function listProjectLifecycles(token, companyUid) {
544
+ const rows = [];
545
+ let cursor;
546
+ // Bounded so a server that keeps returning the same cursor cannot loop forever.
547
+ for (let page = 0; page < 200; page += 1) {
548
+ const data = asRecord(await meshJson(token, "/v1/work-mesh/project-lifecycle", {
549
+ query: { companyUid, ...(cursor ? { cursor } : {}) },
550
+ }));
551
+ const projects = Array.isArray(data?.projects) ? data.projects : [];
552
+ for (const row of projects) {
553
+ const parsed = lifecycleFromRow(row);
554
+ if (parsed)
555
+ rows.push(parsed);
556
+ }
557
+ const next = typeof data?.nextCursor === "string" ? data.nextCursor.trim() : "";
558
+ if (!next || next === cursor)
559
+ break;
560
+ cursor = next;
561
+ }
562
+ return rows;
563
+ }
564
+ /** The lifecycle row for one project, or null when it has never been changed. */
565
+ export async function getProjectLifecycle(token, companyUid, projectId) {
566
+ const rows = await listProjectLifecycles(token, companyUid);
567
+ return rows.find((row) => row.projectId === projectId) ?? null;
568
+ }
569
+ /**
570
+ * PATCH /v1/work-mesh/projects/{id}/lifecycle. `expectedVersion` is the
571
+ * current lifecycle row version (0 when the project has no row yet). The
572
+ * server answers 409 BOARD_NONEMPTY when archiving a Board that still has
573
+ * stories unless `confirmNonempty` is set, and a plain 409 on a version
574
+ * mismatch. Nothing is deleted: stories, folder, channel, members and
575
+ * history stay; the project only leaves the active list and the organize
576
+ * candidates.
577
+ */
578
+ export async function setProjectLifecycle(token, companyUid, projectId, change) {
579
+ const data = await meshJson(token, `/v1/work-mesh/projects/${encodeURIComponent(projectId)}/lifecycle`, {
580
+ method: "PATCH",
581
+ query: { companyUid },
582
+ body: {
583
+ status: change.status,
584
+ expectedVersion: change.expectedVersion,
585
+ ...(change.confirmNonempty ? { confirmNonempty: true } : {}),
586
+ },
587
+ });
588
+ const parsed = lifecycleFromRow(data);
589
+ if (!parsed)
590
+ throw new Error("Work mesh returned an invalid project lifecycle row");
591
+ return parsed;
592
+ }
518
593
  export function pairDmUidsFromInbox(inbox) {
519
594
  const rec = asRecord(inbox);
520
595
  const ids = new Set();
@@ -36,6 +36,8 @@ function allowed(al, c) {
36
36
  const lower = c.secret.toLowerCase();
37
37
  checks.push(al.stopwords.some((s) => lower.includes(s)));
38
38
  }
39
+ if (al.commits.length)
40
+ checks.push(c.commit !== undefined && al.commits.includes(c.commit.toLowerCase()));
39
41
  if (!checks.length)
40
42
  return false;
41
43
  return al.condition === "AND" ? checks.every(Boolean) : checks.some(Boolean);
@@ -50,6 +52,10 @@ function lineBounds(text, start, end) {
50
52
  offset++;
51
53
  return { line: text.slice(from, to), offset };
52
54
  }
55
+ /** A repo allowlist applies to this rule (no targetRules means every rule). */
56
+ function repoAllowed(rules, ruleId, c) {
57
+ return (rules.repo ?? []).some((al) => (!al.targetRules.length || al.targetRules.includes(ruleId)) && allowed(al, c));
58
+ }
53
59
  /** True when the global allowlist excludes this path from scanning. */
54
60
  export function pathIgnored(file, rules = compiledRules()) {
55
61
  return rules.global.paths.some((p) => p.test(file));
@@ -60,6 +66,7 @@ export function scanPath(file, rules = compiledRules()) {
60
66
  return [];
61
67
  return rules.rules
62
68
  .filter((r) => !r.regex && r.path?.test(file))
69
+ .filter((r) => !repoAllowed(rules, r.id, { file, secret: "", match: "", line: "" }))
63
70
  .map((r) => ({ ruleId: r.id, description: r.description, file, line: 1 }));
64
71
  }
65
72
  export function scanFragment(frag, rules = compiledRules()) {
@@ -103,6 +110,8 @@ export function scanFragment(frag, rules = compiledRules()) {
103
110
  continue;
104
111
  if (rule.allowlists.some((al) => allowed(al, cand)))
105
112
  continue;
113
+ if (repoAllowed(rules, rule.id, cand))
114
+ continue;
106
115
  const lineNo = startLine + offset;
107
116
  const key = `${rule.id}:${lineNo}`;
108
117
  if (seen.has(key))
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Repo-level allowlists: `[[allowlists]]` (and the legacy `[allowlist]`) from
3
+ * the scanned repo's own `.gitleaks.toml`, with gitleaks semantics for
4
+ * `paths`, `regexes`, `regexTarget`, `stopwords`, `commits`, `targetRules` and
5
+ * `condition`. They apply on top of the built-in rules and allowlists, to that
6
+ * repo only. Repo `[[rules]]`, `[extend]` and the rest of the file are ignored:
7
+ * a repo can allow known false positives, not change what is detected.
8
+ *
9
+ * The file is read from git, never the working tree:
10
+ * - The staged (index) copy applies only when it is identical to the copy
11
+ * already committed at HEAD (or at MERGE_HEAD during a merge). A commit
12
+ * that changes the allowlist is scanned with the HEAD allowlist, so
13
+ * widening the allowlist and adding the secret cannot happen in one commit.
14
+ * - A changed staged copy is still validated, so a broken file fails now.
15
+ * - A symlinked .gitleaks.toml is rejected.
16
+ *
17
+ * Narrow by construction: every allowlist needs `paths`, each anchored with a
18
+ * literal prefix (`^golden/...`); `regexes` and `stopwords` need
19
+ * `condition = "AND"`, so they never reach files outside those paths.
20
+ *
21
+ * Fails closed: anything not acceptable is a ScanConfigError, and the scan
22
+ * exits 2 rather than running with a guess.
23
+ */
24
+ import { type CompiledAllowlist } from "./rules.js";
25
+ export declare class ScanConfigError extends Error {
26
+ }
27
+ export declare const REPO_CONFIG_FILE = ".gitleaks.toml";
28
+ /**
29
+ * Length of the literal text right after a leading `^`: letters, digits,
30
+ * `/ _ - @ ~` and backslash-escaped punctuation. A quantifier on the last
31
+ * literal makes it optional, so it does not count.
32
+ */
33
+ export declare function literalPathPrefix(src: string): number;
34
+ /** Validated allowlists from the text of a .gitleaks.toml. */
35
+ export declare function parseRepoAllowlists(text: string, file?: string): CompiledAllowlist[];
36
+ /**
37
+ * Compiled allowlists for a staged scan of the repo at `repoRoot`, read from
38
+ * git as described at the top of this file. [] when the repo has none.
39
+ */
40
+ export declare function loadRepoAllowlists(repoRoot: string): CompiledAllowlist[];
41
+ //# sourceMappingURL=repo-config.d.ts.map