@indigoai-us/hq-cli 5.345.156 → 5.345.160

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 (188) hide show
  1. package/CHANGELOG.md +255 -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 +535 -10
  6. package/dist/command-catalog.generated.js +678 -31
  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/access.d.ts +6 -0
  12. package/dist/commands/access.js +18 -1
  13. package/dist/commands/acl-lock.d.ts +41 -0
  14. package/dist/commands/acl-lock.js +28 -0
  15. package/dist/commands/agent-enroll.d.ts +2 -0
  16. package/dist/commands/agent-enroll.js +9 -4
  17. package/dist/commands/agent-kit.js +8 -6
  18. package/dist/commands/agents.js +5 -11
  19. package/dist/commands/billing-sponsor.d.ts +4 -4
  20. package/dist/commands/billing-sponsor.js +11 -11
  21. package/dist/commands/billing-usage.d.ts +22 -8
  22. package/dist/commands/billing-usage.js +57 -22
  23. package/dist/commands/billing.d.ts +7 -7
  24. package/dist/commands/billing.js +13 -13
  25. package/dist/commands/bot.js +7 -4
  26. package/dist/commands/channel-suggest.d.ts +56 -0
  27. package/dist/commands/channel-suggest.js +170 -0
  28. package/dist/commands/channels.js +7 -1
  29. package/dist/commands/cloud-provision.js +2 -2
  30. package/dist/commands/cloud.d.ts +45 -0
  31. package/dist/commands/cloud.js +157 -17
  32. package/dist/commands/company.js +4 -3
  33. package/dist/commands/core-checkpoint-native.d.ts +18 -0
  34. package/dist/commands/core-checkpoint-native.js +67 -2
  35. package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
  36. package/dist/commands/core-pipeline-driver-native.js +131 -5
  37. package/dist/commands/core.js +12 -0
  38. package/dist/commands/crm.js +83 -6
  39. package/dist/commands/db-remote-deps.js +12 -1
  40. package/dist/commands/deploy-access.d.ts +33 -0
  41. package/dist/commands/deploy-access.js +322 -0
  42. package/dist/commands/dm-attach.d.ts +106 -0
  43. package/dist/commands/dm-attach.js +184 -0
  44. package/dist/commands/dm.d.ts +37 -0
  45. package/dist/commands/dm.js +172 -10
  46. package/dist/commands/feedback-show.d.ts +40 -0
  47. package/dist/commands/feedback-show.js +261 -0
  48. package/dist/commands/feedback.d.ts +21 -0
  49. package/dist/commands/feedback.js +152 -2
  50. package/dist/commands/files.d.ts +24 -0
  51. package/dist/commands/files.js +71 -8
  52. package/dist/commands/integrations-connect.js +7 -3
  53. package/dist/commands/lanes-accounts.js +16 -1
  54. package/dist/commands/lanes-loop.js +14 -4
  55. package/dist/commands/lanes-message.js +6 -4
  56. package/dist/commands/lanes-resume.js +36 -13
  57. package/dist/commands/lanes-shared.d.ts +5 -0
  58. package/dist/commands/lanes-shared.js +13 -3
  59. package/dist/commands/lanes-watch.js +1 -0
  60. package/dist/commands/lanes.d.ts +20 -1
  61. package/dist/commands/lanes.js +201 -51
  62. package/dist/commands/meetings.d.ts +4 -4
  63. package/dist/commands/meetings.js +17 -8
  64. package/dist/commands/mesh.js +149 -1
  65. package/dist/commands/monitor.d.ts +1 -0
  66. package/dist/commands/monitor.js +4 -1
  67. package/dist/commands/repo.d.ts +41 -0
  68. package/dist/commands/repo.js +331 -0
  69. package/dist/commands/scan.d.ts +3 -1
  70. package/dist/commands/scan.js +7 -3
  71. package/dist/commands/secrets.d.ts +21 -0
  72. package/dist/commands/secrets.js +114 -14
  73. package/dist/commands/signals.js +17 -5
  74. package/dist/commands/skill.js +6 -6
  75. package/dist/commands/staff/listings.d.ts +41 -0
  76. package/dist/commands/staff/listings.js +160 -0
  77. package/dist/commands/staff/reset-test-account.d.ts +3 -2
  78. package/dist/commands/staff/reset-test-account.js +4 -3
  79. package/dist/commands/whoami.js +1 -1
  80. package/dist/hook-engine-path.js +13 -2
  81. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +3392 -1243
  82. package/dist/hq-cloud-sync-runner/package.json +1 -1
  83. package/dist/lib/agent-kit/run/inbox.js +2 -1
  84. package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
  85. package/dist/lib/agent-kit/wake-secrets.js +32 -0
  86. package/dist/lib/agent-kit/wake.d.ts +4 -0
  87. package/dist/lib/agent-kit/wake.js +49 -3
  88. package/dist/lib/billing/plan-limit-refusals.d.ts +13 -3
  89. package/dist/lib/billing/plan-limit-refusals.js +26 -3
  90. package/dist/lib/billing/plan-limit-upgrade-url.d.ts +8 -0
  91. package/dist/lib/billing/plan-limit-upgrade-url.js +18 -0
  92. package/dist/lib/billing/plan-lock.d.ts +22 -6
  93. package/dist/lib/billing/plan-lock.js +58 -30
  94. package/dist/lib/billing/usage-cost.d.ts +24 -2
  95. package/dist/lib/billing/usage-cost.js +103 -3
  96. package/dist/lib/billing/usage-only.d.ts +37 -1
  97. package/dist/lib/billing/usage-only.js +89 -4
  98. package/dist/lib/billing/usage-pause.d.ts +27 -0
  99. package/dist/lib/billing/usage-pause.js +50 -0
  100. package/dist/lib/bot/inflight.d.ts +7 -1
  101. package/dist/lib/bot/inflight.js +10 -2
  102. package/dist/lib/bot/prompt.js +3 -0
  103. package/dist/lib/bot/run.js +72 -20
  104. package/dist/lib/bot/session.d.ts +14 -0
  105. package/dist/lib/bot/session.js +70 -0
  106. package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
  107. package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
  108. package/dist/lib/capacity/lane-login.d.ts +6 -0
  109. package/dist/lib/capacity/lane-login.js +5 -0
  110. package/dist/lib/core-utils/worktree.js +2 -0
  111. package/dist/lib/daemon/exit-cause.d.ts +43 -0
  112. package/dist/lib/daemon/exit-cause.js +88 -0
  113. package/dist/lib/daemon/handoff.d.ts +2 -0
  114. package/dist/lib/daemon/handoff.js +6 -1
  115. package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
  116. package/dist/lib/daemon/paths.d.ts +2 -0
  117. package/dist/lib/daemon/paths.js +1 -0
  118. package/dist/lib/daemon/run.js +13 -0
  119. package/dist/lib/daemon/supervisor.d.ts +54 -1
  120. package/dist/lib/daemon/supervisor.js +297 -21
  121. package/dist/lib/daemon/unit-holds.d.ts +22 -0
  122. package/dist/lib/daemon/unit-holds.js +86 -0
  123. package/dist/lib/db/control-plane.d.ts +5 -0
  124. package/dist/lib/db/control-plane.js +10 -0
  125. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  126. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  127. package/dist/lib/hook-engine/resolve.d.ts +14 -0
  128. package/dist/lib/hook-engine/resolve.js +55 -1
  129. package/dist/lib/index-render/company-knowledge.js +1 -3
  130. package/dist/lib/index-render/projects.js +1 -3
  131. package/dist/lib/lanes/account-company.d.ts +28 -0
  132. package/dist/lib/lanes/account-company.js +53 -0
  133. package/dist/lib/lanes/accounts.d.ts +22 -3
  134. package/dist/lib/lanes/accounts.js +38 -2
  135. package/dist/lib/lanes/claude-balance.d.ts +6 -1
  136. package/dist/lib/lanes/claude-balance.js +6 -1
  137. package/dist/lib/lanes/claude-credential.d.ts +78 -0
  138. package/dist/lib/lanes/claude-credential.js +163 -0
  139. package/dist/lib/lanes/compaction.d.ts +2 -0
  140. package/dist/lib/lanes/compaction.js +9 -0
  141. package/dist/lib/lanes/config.js +6 -0
  142. package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
  143. package/dist/lib/lanes/hooks-install.js +2 -1
  144. package/dist/lib/lanes/host-capacity.d.ts +2 -0
  145. package/dist/lib/lanes/host-capacity.js +1 -0
  146. package/dist/lib/lanes/keep-alive.d.ts +1 -1
  147. package/dist/lib/lanes/keep-alive.js +3 -1
  148. package/dist/lib/lanes/links.js +10 -4
  149. package/dist/lib/lanes/loop.js +16 -1
  150. package/dist/lib/lanes/node-binary.d.ts +17 -0
  151. package/dist/lib/lanes/node-binary.js +30 -0
  152. package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
  153. package/dist/lib/lanes/outpost-dispatch.js +1 -0
  154. package/dist/lib/lanes/reconcile.js +4 -0
  155. package/dist/lib/lanes/remote-control-lane.js +2 -0
  156. package/dist/lib/lanes/spawn.d.ts +3 -0
  157. package/dist/lib/lanes/spawn.js +119 -9
  158. package/dist/lib/lanes/store.d.ts +6 -0
  159. package/dist/lib/lanes/store.js +107 -18
  160. package/dist/lib/lanes/types.d.ts +2 -0
  161. package/dist/lib/lanes/watch.js +28 -5
  162. package/dist/lib/lanes/watcher-main.js +4 -3
  163. package/dist/lib/lanes/watcher.d.ts +4 -0
  164. package/dist/lib/lanes/watcher.js +110 -21
  165. package/dist/lib/mesh/api.d.ts +32 -0
  166. package/dist/lib/mesh/api.js +75 -0
  167. package/dist/lib/plan-limit-nag.d.ts +0 -16
  168. package/dist/lib/plan-limit-nag.js +17 -18
  169. package/dist/lib/secret-scan/detect.js +9 -0
  170. package/dist/lib/secret-scan/repo-config.d.ts +41 -0
  171. package/dist/lib/secret-scan/repo-config.js +237 -0
  172. package/dist/lib/secret-scan/rules.d.ts +7 -1
  173. package/dist/lib/secret-scan/rules.js +3 -1
  174. package/dist/lib/secret-scan/staged.d.ts +5 -0
  175. package/dist/lib/secret-scan/staged.js +19 -2
  176. package/dist/lib/secret-scan/types.d.ts +2 -0
  177. package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
  178. package/dist/lib/sync/server-owned-local-only.js +75 -0
  179. package/dist/main.d.ts +0 -2
  180. package/dist/main.js +19 -57
  181. package/dist/utils/package-use-lease.js +27 -9
  182. package/dist/utils/plan-gate-error.d.ts +30 -14
  183. package/dist/utils/plan-gate-error.js +60 -46
  184. package/dist/utils/team-upgrade.d.ts +19 -6
  185. package/dist/utils/team-upgrade.js +37 -39
  186. package/dist/utils/version-gate.d.ts +7 -0
  187. package/dist/utils/version-gate.js +66 -0
  188. package/package.json +8 -8
@@ -24,7 +24,13 @@ export const LANES_STORE_REL = path.join("workspace", "lanes");
24
24
  export const LANE_RECORDS_DIR = "lanes";
25
25
  export const QUESTION_RECORDS_DIR = "questions";
26
26
  export const PENDING_INDEX_DIR = path.join("questions", "index", "pending");
27
- const LOCK_STALE_MS = 30_000;
27
+ const DEFAULT_LOCK_STALE_MS = 30_000;
28
+ /** Lock age after which any holder is presumed gone. HQ_LANES_LOCK_STALE_MS overrides it. */
29
+ function lockStaleMs(env = process.env) {
30
+ const raw = env.HQ_LANES_LOCK_STALE_MS;
31
+ const parsed = raw === undefined ? NaN : Number(raw);
32
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : DEFAULT_LOCK_STALE_MS;
33
+ }
28
34
  const LOCK_RETRY_MS = 10;
29
35
  const LOCK_TIMEOUT_MS = 5_000;
30
36
  function defaultStoreDeps() {
@@ -153,16 +159,104 @@ function wait(ms) {
153
159
  function lockPathFor(recordPath) {
154
160
  return `${recordPath}.lock`;
155
161
  }
156
- function withRecordLock(recordPath, deps, fn) {
162
+ function lockOwnerAlive(pid) {
163
+ try {
164
+ process.kill(pid, 0);
165
+ return true;
166
+ }
167
+ catch (err) {
168
+ return err.code !== "ESRCH";
169
+ }
170
+ }
171
+ function sameFile(a, b) {
172
+ return a.ino === b.ino && a.dev === b.dev;
173
+ }
174
+ /**
175
+ * Remove a record lock that no live process holds, and report whether it was
176
+ * removed. A lock is stale when it was written on this host and its owner pid
177
+ * no longer exists, or when it is older than the stale window. Age is the
178
+ * larger of the timestamp age and the file mtime age, so a future or
179
+ * unparseable timestamp still ages out through mtime.
180
+ *
181
+ * Removal is atomic: the lock is renamed to a unique name, which only one
182
+ * reclaimer can win. If the renamed file is not the lock that was inspected
183
+ * (another process reclaimed it and took a fresh lock in between), the fresh
184
+ * lock is linked back into place and nothing is reclaimed.
185
+ */
186
+ function recordLockIsStale(lockPath, deps) {
187
+ const statSync = deps.statSync ?? ((p) => fs.statSync(p));
188
+ const linkSync = deps.linkSync ?? fs.linkSync.bind(fs);
189
+ const before = statSync(lockPath);
190
+ const raw = deps.readFileSync(lockPath, "utf8");
191
+ const [pidLine, tsLine, hostLine] = raw.split("\n");
192
+ const pid = pidLine ? Number(pidLine) : NaN;
193
+ const ts = tsLine ? Number(tsLine) : NaN;
194
+ const pidKnown = Number.isSafeInteger(pid) && pid > 0;
195
+ // A pid is only meaningful on the host that wrote it. A lock with a foreign
196
+ // or missing host falls back to the age rule.
197
+ const sameHost = hostLine !== undefined && hostLine.trim() !== "" && hostLine.trim() === deps.hostname();
198
+ const now = Date.now();
199
+ const mtimeAge = now - before.mtimeMs;
200
+ const age = Number.isFinite(ts) ? Math.max(now - ts, mtimeAge) : mtimeAge;
201
+ const stale = (pidKnown && sameHost && pid !== process.pid && !lockOwnerAlive(pid)) ||
202
+ age > lockStaleMs();
203
+ if (!stale)
204
+ return false;
205
+ const parked = `${lockPath}.stale.${process.pid}.${randomBytes(6).toString("hex")}`;
206
+ try {
207
+ deps.renameSync(lockPath, parked);
208
+ }
209
+ catch (renameErr) {
210
+ // Another reclaimer won the rename, or the holder released it.
211
+ if (renameErr.code === "ENOENT")
212
+ return false;
213
+ throw renameErr;
214
+ }
215
+ const removeParked = () => {
216
+ try {
217
+ deps.unlinkSync(parked);
218
+ }
219
+ catch (unlinkErr) {
220
+ if (unlinkErr.code !== "ENOENT")
221
+ throw unlinkErr;
222
+ }
223
+ };
224
+ let reclaimed;
225
+ try {
226
+ reclaimed = sameFile(statSync(parked), before) && deps.readFileSync(parked, "utf8") === raw;
227
+ if (!reclaimed) {
228
+ // The parked file is a live holder's fresh lock. Put it back.
229
+ try {
230
+ linkSync(parked, lockPath);
231
+ }
232
+ catch (linkErr) {
233
+ if (linkErr.code !== "EEXIST")
234
+ throw linkErr;
235
+ process.stderr.write(`lanes store: could not restore displaced lock ${lockPath}; a newer lock already exists\n`);
236
+ }
237
+ }
238
+ }
239
+ catch (err) {
240
+ removeParked();
241
+ throw err;
242
+ }
243
+ removeParked();
244
+ return reclaimed;
245
+ }
246
+ function withRecordLock(recordPath, deps, fn, timeoutMs = LOCK_TIMEOUT_MS) {
157
247
  const lockPath = lockPathFor(recordPath);
248
+ const statSync = deps.statSync ?? ((p) => fs.statSync(p));
249
+ const fstatSync = deps.fstatSync ?? ((fd) => fs.fstatSync(fd));
158
250
  ensureDir(path.dirname(recordPath), deps);
159
251
  const started = Date.now();
160
252
  while (true) {
161
253
  let fd;
162
254
  try {
163
255
  fd = deps.openSync(lockPath, "wx");
256
+ let held;
164
257
  try {
165
- fs.writeSync(fd, Buffer.from(`${process.pid}\n${Date.now()}\n`, "utf8"));
258
+ fs.writeSync(fd, Buffer.from(`${process.pid}\n${Date.now()}\n${deps.hostname()}\n`, "utf8"));
259
+ held = fstatSync(fd);
166
260
  }
167
261
  catch (err) {
168
262
  deps.closeSync(fd);
@@ -180,7 +274,10 @@ function withRecordLock(recordPath, deps, fn) {
180
274
  process.stderr.write(`lanes store: lock close failed for ${lockPath}: ${err instanceof Error ? err.message : String(err)}\n`);
181
275
  }
182
276
  try {
183
- deps.unlinkSync(lockPath);
277
+ // Unlink only our own lock. If it was reclaimed and replaced while
278
+ // fn ran, the file at lockPath belongs to the successor.
279
+ if (sameFile(statSync(lockPath), held))
280
+ deps.unlinkSync(lockPath);
184
281
  }
185
282
  catch (err) {
186
283
  const code = err.code;
@@ -194,24 +291,12 @@ function withRecordLock(recordPath, deps, fn) {
194
291
  const code = err.code;
195
292
  if (code !== "EEXIST")
196
293
  throw err;
197
- if (Date.now() - started > LOCK_TIMEOUT_MS) {
294
+ if (Date.now() - started > timeoutMs) {
198
295
  throw new LanesError("store_lock_timeout", `Timed out waiting for lock ${lockPath}`, { path: lockPath });
199
296
  }
200
297
  try {
201
- const raw = deps.readFileSync(lockPath, "utf8");
202
- const tsLine = raw.split("\n")[1];
203
- const ts = tsLine ? Number(tsLine) : NaN;
204
- if (Number.isFinite(ts) && Date.now() - ts > LOCK_STALE_MS) {
205
- try {
206
- deps.unlinkSync(lockPath);
207
- }
208
- catch (unlinkErr) {
209
- const unlinkCode = unlinkErr.code;
210
- if (unlinkCode !== "ENOENT")
211
- throw unlinkErr;
212
- }
298
+ if (recordLockIsStale(lockPath, deps))
213
299
  continue;
214
- }
215
300
  }
216
301
  catch (statErr) {
217
302
  const statCode = statErr.code;
@@ -223,6 +308,10 @@ function withRecordLock(recordPath, deps, fn) {
223
308
  }
224
309
  }
225
310
  }
311
+ /** Test seam: run withRecordLock with overridden deps and a shorter timeout. */
312
+ export function withRecordLockForTests(recordPath, overrides, fn, timeoutMs) {
313
+ return withRecordLock(recordPath, { ...defaultStoreDeps(), ...overrides }, fn, timeoutMs);
314
+ }
226
315
  /** Run a lane-owned file read/modify/write transaction under its sidecar lock. */
227
316
  export function withLaneRecordFileLock(recordPath, fn) {
228
317
  return withRecordLock(recordPath, defaultStoreDeps(), fn);
@@ -1029,6 +1029,8 @@ export interface LaneEnvelope {
1029
1029
  fingerprint?: string | null;
1030
1030
  }
1031
1031
  export interface AccountConfig {
1032
+ /** Company slug or UID owning this account. Untagged accounts are personal. */
1033
+ company?: string;
1032
1034
  home: string;
1033
1035
  /** Optional account home overrides keyed by the owning host's hostname. */
1034
1036
  hosts?: Record<string, AccountHostHome>;
@@ -1,3 +1,7 @@
1
+ import * as path from "node:path";
2
+ import * as fs from "node:fs";
3
+ import { loopControllerIsAlive, loopStatus } from "./loop.js";
4
+ import { laneRunDir } from "./spawn.js";
1
5
  import { expandLocalLaneIdPrefix, listQuestionIds, readLane, readQuestion, } from "./store.js";
2
6
  import { LaneNotFoundError, LanesError } from "./errors.js";
3
7
  import { providerSupportsSessionResume } from "./delivery.js";
@@ -118,6 +122,7 @@ export async function runLaneWatch(hqRoot, laneIds, deps = {}) {
118
122
  while (true) {
119
123
  polls += 1;
120
124
  const records = [];
125
+ const activeLoops = new Set();
121
126
  for (const laneId of watchedIds) {
122
127
  let record = null;
123
128
  try {
@@ -131,9 +136,27 @@ export async function runLaneWatch(hqRoot, laneIds, deps = {}) {
131
136
  continue;
132
137
  }
133
138
  records.push(record);
134
- if (previousStates.get(laneId) !== record.state) {
135
- emit(`lane ${record.lane_id}: state=${record.state}${isTerminalLaneState(record.state) ? " (terminal)" : ""}`);
136
- previousStates.set(laneId, record.state);
139
+ if (record.loop_mode === true) {
140
+ try {
141
+ const runDir = laneRunDir(hqRoot, record.lane_id);
142
+ // loopStatus defaults missing metadata to waiting; that is not
143
+ // evidence of a controller after a failed admission or launch.
144
+ if (fs.existsSync(path.join(runDir, "loop", "loop.json"))) {
145
+ const status = loopStatus(runDir, record.lane_id);
146
+ if (status.state !== "stopped" && status.state !== "failed" && loopControllerIsAlive(runDir))
147
+ activeLoops.add(record.lane_id);
148
+ }
149
+ }
150
+ catch (err) {
151
+ unreadable("loop", laneId, err);
152
+ }
153
+ }
154
+ // A phase can be done while its loop controller awaits more work.
155
+ const terminal = isTerminalLaneState(record.state) && !activeLoops.has(record.lane_id);
156
+ const stateKey = `${record.state}:${terminal}`;
157
+ if (previousStates.get(laneId) !== stateKey) {
158
+ emit(`lane ${record.lane_id}: state=${record.state}${terminal ? " (terminal)" : ""}`);
159
+ previousStates.set(laneId, stateKey);
137
160
  }
138
161
  }
139
162
  const keepAliveWaiting = new Set();
@@ -175,7 +198,7 @@ export async function runLaneWatch(hqRoot, laneIds, deps = {}) {
175
198
  }
176
199
  if (deps.readAccountStatuses !== undefined) {
177
200
  const accountLanes = records.filter((record) => record.account !== undefined &&
178
- (!isTerminalLaneState(record.state) || keepAliveWaiting.has(record.lane_id)));
201
+ (!isTerminalLaneState(record.state) || activeLoops.has(record.lane_id) || keepAliveWaiting.has(record.lane_id)));
179
202
  let statuses;
180
203
  try {
181
204
  statuses = await deps.readAccountStatuses(accountLanes);
@@ -242,7 +265,7 @@ export async function runLaneWatch(hqRoot, laneIds, deps = {}) {
242
265
  previousAccountLogin.set(key, status.logged_in);
243
266
  }
244
267
  }
245
- if (records.every((record) => isTerminalLaneState(record.state) && !keepAliveWaiting.has(record.lane_id))) {
268
+ if (records.every((record) => isTerminalLaneState(record.state) && !activeLoops.has(record.lane_id) && !keepAliveWaiting.has(record.lane_id))) {
246
269
  return { polls, lines, suppressed };
247
270
  }
248
271
  await sleep(intervalMs);
@@ -12,6 +12,7 @@ import { isMainModule } from "../../utils/is-main-module.js";
12
12
  import { runWatcherLoop } from "./watcher.js";
13
13
  import { spawnEnvelopeRepairViaCurrentCli, } from "./envelope-repair-dispatch.js";
14
14
  import { LanesError } from "./errors.js";
15
+ import { liveNodeBinary } from "./node-binary.js";
15
16
  import { openMeshTransport } from "../mesh/client.js";
16
17
  import { CompanySelectionError } from "../../utils/company-selection-error.js";
17
18
  export function watcherMainDeps(hqRoot, laneId, env = process.env, repairDispatchDeps = {}) {
@@ -21,7 +22,7 @@ export function watcherMainDeps(hqRoot, laneId, env = process.env, repairDispatc
21
22
  spawnRepair: (root, lane, input) => spawnEnvelopeRepairViaCurrentCli(root, lane, input, { ...repairDispatchDeps, env }),
22
23
  deliverLaneSeniorMessage: async (seniorLaneId, text) => deliverLaneSeniorMessage(cliEntry, seniorLaneId, text),
23
24
  recoverQueuedMessages: async () => {
24
- const result = spawnSync(process.execPath, [cliEntry, "lanes", "_recover-queued-messages", laneId], {
25
+ const result = spawnSync(liveNodeBinary(), [cliEntry, "lanes", "_recover-queued-messages", laneId], {
25
26
  env,
26
27
  encoding: "utf8",
27
28
  timeout: 120_000,
@@ -34,7 +35,7 @@ export function watcherMainDeps(hqRoot, laneId, env = process.env, repairDispatc
34
35
  },
35
36
  scheduleTerminalKeepAlive: async ({ laneId: scheduledLaneId, version, dueAt }) => {
36
37
  await new Promise((resolve, reject) => {
37
- const child = spawn(process.execPath, [
38
+ const child = spawn(liveNodeBinary(), [
38
39
  cliEntry,
39
40
  "lanes",
40
41
  "_keep-alive-terminal-trigger",
@@ -54,7 +55,7 @@ export function watcherMainDeps(hqRoot, laneId, env = process.env, repairDispatc
54
55
  };
55
56
  }
56
57
  function deliverLaneSeniorMessage(cliEntry, laneId, text) {
57
- const result = spawnSync(process.execPath, [cliEntry, "lanes", "message", laneId, "--text", text], {
58
+ const result = spawnSync(liveNodeBinary(), [cliEntry, "lanes", "message", laneId, "--text", text], {
58
59
  env: process.env,
59
60
  encoding: "utf8",
60
61
  timeout: 120_000,
@@ -201,6 +201,8 @@ export interface WatcherLoopDeps {
201
201
  progressEventMinIntervalMs?: number;
202
202
  /** Bound a hung mesh request so watcher liveness polling continues. */
203
203
  meshRequestTimeoutMs?: number;
204
+ /** Bound on waiting for an older run's watcher to release the claim. */
205
+ claimWaitMs?: number;
204
206
  /** Spec §4 repair spawn. Default: `spawnEnvelopeRepair`. */
205
207
  spawnRepair?: (hqRoot: string, lane: LaneRecord, input: EnvelopeRepairSpawnInput) => number;
206
208
  /** Rebind and respawn after a provider quota or authentication refusal. */
@@ -247,6 +249,8 @@ export interface WatcherLoopDeps {
247
249
  export declare function emitMeshSoft(event: string, logDecision: (record: Record<string, unknown>) => void, at: string, fn: (signal: AbortSignal) => Promise<void>, timeoutMs?: number): Promise<boolean>;
248
250
  /** Question ids an ask envelope raised, as `raiseEnvelopeQuestions` mints them. */
249
251
  export declare function envelopeQuestionIds(laneId: string, envelopeRef: string, envelope: LaneEnvelope, questionIndexes?: readonly number[]): string[];
252
+ /** How long a new phase watcher waits for an older phase's watcher to let go. */
253
+ export declare const WATCHER_CLAIM_WAIT_MS = 60000;
250
254
  /** Detached and synchronous classifiers share one process-identity claim. */
251
255
  export declare function runWatcherLoop(hqRoot: string, laneId: string, deps?: WatcherLoopDeps): Promise<void>;
252
256
  export type EnvelopeRepairDecision = {
@@ -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;