@indigoai-us/hq-cli 5.345.123 → 5.345.125

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 (44) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/assets/scaffold/core/scripts/work-mesh-project-registration-offer.sh +77 -0
  3. package/assets/scaffold/core/scripts/workflow-gate.sh +247 -0
  4. package/dist/command-catalog.generated.d.ts +239 -0
  5. package/dist/command-catalog.generated.js +291 -0
  6. package/dist/commands/core-bucket-c-work-mesh-offer-native.d.ts +7 -0
  7. package/dist/commands/core-bucket-c-work-mesh-offer-native.js +147 -0
  8. package/dist/commands/core-bucket-c-workflow-gate-fast.d.ts +3 -0
  9. package/dist/commands/core-bucket-c-workflow-gate-fast.js +17 -0
  10. package/dist/commands/core-bucket-c-workflow-gate-native.d.ts +7 -0
  11. package/dist/commands/core-bucket-c-workflow-gate-native.js +250 -0
  12. package/dist/commands/core.js +12 -0
  13. package/dist/commands/lanes-accounts.js +22 -3
  14. package/dist/commands/lanes.js +25 -32
  15. package/dist/commands/mesh.js +457 -16
  16. package/dist/fast-scaffold.js +26 -0
  17. package/dist/lib/lanes/accounts.d.ts +48 -2
  18. package/dist/lib/lanes/accounts.js +160 -9
  19. package/dist/lib/lanes/index.d.ts +2 -2
  20. package/dist/lib/lanes/index.js +1 -1
  21. package/dist/lib/lanes/message-replay.js +6 -1
  22. package/dist/lib/lanes/provider-rejection.d.ts +2 -6
  23. package/dist/lib/lanes/provider-rejection.js +19 -0
  24. package/dist/lib/lanes/quota-notice.d.ts +24 -0
  25. package/dist/lib/lanes/quota-notice.js +61 -0
  26. package/dist/lib/lanes/quota.d.ts +55 -15
  27. package/dist/lib/lanes/quota.js +231 -49
  28. package/dist/lib/lanes/reconcile.js +17 -1
  29. package/dist/lib/lanes/session-relink.d.ts +2 -0
  30. package/dist/lib/lanes/session-relink.js +98 -4
  31. package/dist/lib/lanes/spawn.d.ts +1 -1
  32. package/dist/lib/lanes/spawn.js +58 -12
  33. package/dist/lib/lanes/types.d.ts +5 -0
  34. package/dist/lib/lanes/types.js +1 -1
  35. package/dist/lib/lanes/watcher.js +13 -1
  36. package/dist/lib/mesh/api.d.ts +2 -0
  37. package/dist/lib/mesh/api.js +3 -0
  38. package/dist/lib/mesh/client.d.ts +6 -1
  39. package/dist/lib/mesh/client.js +18 -5
  40. package/dist/lib/mesh/team-coordination.d.ts +106 -0
  41. package/dist/lib/mesh/team-coordination.js +297 -0
  42. package/dist/lib/work-context/auto-bind.d.ts +75 -0
  43. package/dist/lib/work-context/auto-bind.js +245 -0
  44. package/package.json +1 -1
@@ -117,8 +117,102 @@ function uniqueHomes(homes) {
117
117
  }
118
118
  return [...unique];
119
119
  }
120
- function missingTranscript(input, homes) {
121
- return new LanesError("provider_transcript_missing", `Provider transcript for session ${input.sessionId} was not found. Homes searched: ${homes.join(", ")}`, { provider: input.provider, session_id: input.sessionId, homes_searched: homes });
120
+ function missingTranscript(input, homes, dirsScanned) {
121
+ return new LanesError("provider_transcript_missing", `Provider transcript for session ${input.sessionId} was not found. Homes searched: ${homes.join(", ")}. Directories scanned: ${dirsScanned.length > 0 ? dirsScanned.join(", ") : "none"}`, {
122
+ provider: input.provider,
123
+ session_id: input.sessionId,
124
+ homes_searched: homes,
125
+ directories_scanned: dirsScanned,
126
+ });
127
+ }
128
+ /**
129
+ * Every `<home>/projects/<dir>/` that could hold a claude transcript. A lane
130
+ * can record one cwd and have run in another (a worktree), so a resume looks
131
+ * in every project directory before refusing.
132
+ */
133
+ function claudeProjectDirs(home) {
134
+ const root = path.join(home, "projects");
135
+ let entries;
136
+ try {
137
+ entries = fs.readdirSync(root, { withFileTypes: true });
138
+ }
139
+ catch (err) {
140
+ const code = err.code;
141
+ if (code === "ENOENT" || code === "ENOTDIR")
142
+ return [];
143
+ throw new LanesError("provider_home_search_failed", `Could not search provider home ${home}: ${err instanceof Error ? err.message : String(err)}`, { provider: "claude", home });
144
+ }
145
+ return entries
146
+ .filter((entry) => entry.isDirectory())
147
+ .map((entry) => path.join(root, entry.name))
148
+ .sort();
149
+ }
150
+ function isFile(target) {
151
+ try {
152
+ return fs.statSync(target).isFile();
153
+ }
154
+ catch (err) {
155
+ const code = err.code;
156
+ if (code === "ENOENT" || code === "ENOTDIR")
157
+ return false;
158
+ throw err;
159
+ }
160
+ }
161
+ /**
162
+ * Last resort for a claude session that is not under the expected project
163
+ * directory in any home: scan every project directory of every searched home,
164
+ * then copy the transcript (and its sibling subagent directory) into the
165
+ * target home under the project directory for `input.cwd`, which is where
166
+ * `claude --resume` started in that cwd looks for it.
167
+ */
168
+ function relinkFromAnyProject(input, targetHome, searched) {
169
+ const scanned = [];
170
+ if (input.provider !== "claude") {
171
+ for (const home of searched)
172
+ scanned.push(path.join(home, "sessions"));
173
+ throw missingTranscript(input, searched, scanned);
174
+ }
175
+ for (const home of searched) {
176
+ const found = [];
177
+ const dirs = claudeProjectDirs(home);
178
+ if (dirs.length === 0)
179
+ scanned.push(path.join(home, "projects"));
180
+ for (const dir of dirs) {
181
+ scanned.push(dir);
182
+ const candidate = path.join(dir, `${input.sessionId}.jsonl`);
183
+ if (isFile(candidate))
184
+ found.push(candidate);
185
+ }
186
+ if (found.length === 0)
187
+ continue;
188
+ if (found.length > 1) {
189
+ throw new LanesError("provider_transcript_ambiguous", `Multiple claude transcripts for session ${input.sessionId} exist in ${home}`, { provider: input.provider, session_id: input.sessionId, homes_searched: searched, matches: found });
190
+ }
191
+ const source = found[0];
192
+ const targetDir = path.join(targetHome, "projects", encodeClaudeProjectDir(input.cwd));
193
+ const target = path.join(targetDir, `${input.sessionId}.jsonl`);
194
+ const subagents = path.join(path.dirname(source), input.sessionId);
195
+ const hasSubagents = statDirectory(subagents);
196
+ const plan = {
197
+ provider: "claude",
198
+ sources: [source],
199
+ targets: [target],
200
+ source_dir: hasSubagents ? subagents : null,
201
+ target_dir: hasSubagents ? path.join(targetDir, input.sessionId) : null,
202
+ };
203
+ const copied = copySessionFiles(plan);
204
+ if (!isFile(target)) {
205
+ throw new LanesError("provider_transcript_relink_failed", `Could not verify session ${input.sessionId} in target home ${targetHome}`, { provider: input.provider, session_id: input.sessionId, target_home: targetHome });
206
+ }
207
+ return {
208
+ ...copied,
209
+ source_home: home,
210
+ transcript_path: target,
211
+ homes_searched: searched,
212
+ found_outside_cwd: true,
213
+ };
214
+ }
215
+ throw missingTranscript(input, searched, scanned);
122
216
  }
123
217
  function copyVerifiedTranscript(input, sourceHome, targetHome, searched) {
124
218
  let copied;
@@ -132,7 +226,7 @@ function copyVerifiedTranscript(input, sourceHome, targetHome, searched) {
132
226
  }
133
227
  catch (err) {
134
228
  if (err instanceof LanesError && err.error === "provider_session_missing") {
135
- throw missingTranscript(input, searched);
229
+ return relinkFromAnyProject(input, targetHome, searched);
136
230
  }
137
231
  throw err;
138
232
  }
@@ -202,7 +296,7 @@ export function ensureSessionHome(input) {
202
296
  matches.push({ home, transcript: found[0] });
203
297
  }
204
298
  if (matches.length === 0)
205
- throw missingTranscript(input, searched);
299
+ return relinkFromAnyProject(input, targetHome, searched);
206
300
  if (matches.length > 1) {
207
301
  throw new LanesError("provider_transcript_ambiguous", `Provider transcript for session ${input.sessionId} exists in multiple homes: ${matches.map((match) => match.home).join(", ")}`, {
208
302
  provider: input.provider,
@@ -255,7 +255,7 @@ export declare function buildProviderCommand(provider: LaneProvider, input: {
255
255
  export declare function workerEnv(base: NodeJS.ProcessEnv, resolved: {
256
256
  envVar: string;
257
257
  home: string;
258
- }, laneId: string, hqRoot: string, autoCompactTokens?: number, meshCompany?: LaneCompanyRef, workerPolicyDir?: string, registryWorker?: LaneRecord["registry_worker"]): NodeJS.ProcessEnv;
258
+ }, laneId: string, hqRoot: string, autoCompactTokens?: number, meshCompany?: LaneCompanyRef, workerPolicyDir?: string, registryWorker?: LaneRecord["registry_worker"], homedir?: () => string): NodeJS.ProcessEnv;
259
259
  export declare const WORKER_POLICY_HOOK_CAPABILITY = "# hq-capability: worker-policy-dir v1";
260
260
  /** Grok does not run HQ hooks; Claude and Codex use the installed capability marker. */
261
261
  export declare function workerPolicyDeliveryMode(hqRoot: string, provider: LaneProvider): "hook" | "prompt";
@@ -13,7 +13,7 @@ import * as fs from "node:fs";
13
13
  import * as path from "node:path";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import { configureRemoteControlChildEnv, prepareRemoteControlLane, remoteControlSelectionConfig } from "./remote-control-lane.js";
16
- import { PROVIDER_HOME_ENV, accountStatus, isLoggedOut, resolveAccount, selectBalancedAccount, } from "./accounts.js";
16
+ import { PROVIDER_HOME_ENV, accountStatus, isDefaultProviderHome, isLoggedOut, resolveAccount, selectBalancedAccount, } from "./accounts.js";
17
17
  import { effectiveAccountHome } from "./account-home.js";
18
18
  import { buildHostCapacitySnapshot } from "./host-capacity.js";
19
19
  import { selectClaudeAccount } from "./claude-balance.js";
@@ -1661,7 +1661,7 @@ export function buildProviderCommand(provider, input) {
1661
1661
  args: appendFastModeFlags(parsed, appendEffortFlag(parsed, appendModelFlag(parsed, appendCodexWorkerPolicyDirFlag(parsed, appendProviderProfileFlags(parsed, args, input.profile), input.workerPolicyDir), input.model), input.effort), input.fastMode),
1662
1662
  };
1663
1663
  }
1664
- export function workerEnv(base, resolved, laneId, hqRoot, autoCompactTokens, meshCompany, workerPolicyDir, registryWorker) {
1664
+ export function workerEnv(base, resolved, laneId, hqRoot, autoCompactTokens, meshCompany, workerPolicyDir, registryWorker, homedir) {
1665
1665
  const accepted = Object.values(PROVIDER_HOME_ENV);
1666
1666
  if (!accepted.includes(resolved.envVar)) {
1667
1667
  throw new LanesError("unknown_provider_home_env", `Unknown provider home variable ${JSON.stringify(resolved.envVar)}. Accepted: ${accepted.join(", ")}`, { accepted, value: resolved.envVar });
@@ -1672,7 +1672,16 @@ export function workerEnv(base, resolved, laneId, hqRoot, autoCompactTokens, mes
1672
1672
  delete env.HQ_POLICY_WORKER_DIR;
1673
1673
  delete env.HQ_LANE_APPROVAL_REQUIRED;
1674
1674
  delete env.HQ_LANE_HUMAN_CHECKPOINTS;
1675
- env[resolved.envVar] = resolved.home;
1675
+ if (resolved.envVar === PROVIDER_HOME_ENV.claude &&
1676
+ isDefaultProviderHome("claude", resolved.home, homedir)) {
1677
+ // The default ~/.claude home runs with CLAUDE_CONFIG_DIR unset so the
1678
+ // worker reads the same keychain credential as the interactive session
1679
+ // (see isDefaultProviderHome). An inherited value is dropped too.
1680
+ delete env[resolved.envVar];
1681
+ }
1682
+ else {
1683
+ env[resolved.envVar] = resolved.home;
1684
+ }
1676
1685
  delete env[CLAUDE_AUTO_COMPACT_ENV];
1677
1686
  if (resolved.envVar === PROVIDER_HOME_ENV.claude && autoCompactTokens !== undefined) {
1678
1687
  env[CLAUDE_AUTO_COMPACT_ENV] = String(autoCompactTokens);
@@ -2075,6 +2084,14 @@ export function readSpawnManifest(runDir) {
2075
2084
  }
2076
2085
  return out;
2077
2086
  }
2087
+ function isExistingDirectory(target) {
2088
+ try {
2089
+ return fs.statSync(target).isDirectory();
2090
+ }
2091
+ catch {
2092
+ return false;
2093
+ }
2094
+ }
2078
2095
  function laneLaunchResult(lane) {
2079
2096
  const { provider_home_at_spawn: _providerHomeAtSpawn, ...safe } = lane;
2080
2097
  return safe;
@@ -2327,6 +2344,17 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
2327
2344
  : existing;
2328
2345
  throw new LanesError("account_not_logged_in", `Account ${resolved.id} for ${lane.provider} is not logged in (no credential file)`, { provider: lane.provider, account: resolved.id, lane_id: lane.lane_id, lane: failed });
2329
2346
  }
2347
+ // The worker starts here, and claude keys its transcript by this directory,
2348
+ // so the transcript checks below use the same value. A resume reuses the
2349
+ // directory the last run started in (a worktree for pipeline lanes) when it
2350
+ // still exists.
2351
+ const recordedWorkingDirectory = plan.mode === "resume" &&
2352
+ typeof existing.working_directory_at_spawn === "string" &&
2353
+ existing.working_directory_at_spawn.trim() !== "" &&
2354
+ isExistingDirectory(existing.working_directory_at_spawn)
2355
+ ? existing.working_directory_at_spawn
2356
+ : undefined;
2357
+ const workerCwd = deps.workingDirectory ?? pendingResume?.working_directory ?? recordedWorkingDirectory ?? hqRoot;
2330
2358
  if (existing.account !== resolved.id) {
2331
2359
  const source = existing.account
2332
2360
  ? config.providers[existing.provider].accounts[existing.account]
@@ -2335,12 +2363,22 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
2335
2363
  source &&
2336
2364
  existing.provider_session_id &&
2337
2365
  providerSupportsSessionResume(existing.provider)) {
2338
- copySessionFiles(planSessionCopy(existing.provider, {
2339
- sourceHome: existing.provider_home_at_spawn ?? effectiveAccountHome(source, hostname).home,
2340
- targetHome: resolved.home,
2341
- cwd: hqRoot,
2342
- sessionId: existing.provider_session_id,
2343
- }));
2366
+ try {
2367
+ copySessionFiles(planSessionCopy(existing.provider, {
2368
+ sourceHome: existing.provider_home_at_spawn ?? effectiveAccountHome(source, hostname).home,
2369
+ targetHome: resolved.home,
2370
+ cwd: workerCwd,
2371
+ sessionId: existing.provider_session_id,
2372
+ }));
2373
+ }
2374
+ catch (err) {
2375
+ // A resume below searches every home and project directory for the
2376
+ // session, so a transcript under another cwd is handled there.
2377
+ const resumeFollows = plan.mode === "resume" && existing.provider !== "grok";
2378
+ if (!(resumeFollows && err instanceof LanesError && err.error === "provider_session_missing")) {
2379
+ throw err;
2380
+ }
2381
+ }
2344
2382
  }
2345
2383
  }
2346
2384
  if (plan.mode === "resume" && existing.provider !== "grok") {
@@ -2350,7 +2388,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
2350
2388
  ensureSessionHome({
2351
2389
  provider: existing.provider,
2352
2390
  sessionId: plan.session_id,
2353
- cwd: hqRoot,
2391
+ cwd: workerCwd,
2354
2392
  targetHome,
2355
2393
  registeredHomes: Object.values(config.providers[existing.provider].accounts)
2356
2394
  .map((account) => effectiveAccountHome(account, hostname).home),
@@ -2644,6 +2682,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
2644
2682
  pending_settings: undefined,
2645
2683
  account: resolved.id,
2646
2684
  provider_home_at_spawn: remoteControlHome ?? resolved.home,
2685
+ working_directory_at_spawn: workerCwd,
2647
2686
  ...(model !== undefined ? { model } : {}),
2648
2687
  ...(model !== undefined
2649
2688
  ? {
@@ -2789,7 +2828,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
2789
2828
  let watcherPid;
2790
2829
  try {
2791
2830
  noteCapacityAdvisory(lane, env);
2792
- workerPid = spawnTrackedWorker(spawnProcess, command.command, command.args, childEnv, deps.workingDirectory ?? pendingResume?.working_directory ?? hqRoot, path.join(logsDir, "worker.out"), path.join(logsDir, "worker.err"), path.join(logsDir, "worker-exit.json"));
2831
+ workerPid = spawnTrackedWorker(spawnProcess, command.command, command.args, childEnv, workerCwd, path.join(logsDir, "worker.out"), path.join(logsDir, "worker.err"), path.join(logsDir, "worker-exit.json"));
2793
2832
  writePidRecord(path.join(runDir, "worker.pid"), {
2794
2833
  pid: workerPid,
2795
2834
  starttime: readProcStarttime(workerPid) ?? undefined,
@@ -3095,12 +3134,19 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
3095
3134
  ? { resume: { session_id: sessionId, prompt: repairPrompt } }
3096
3135
  : { freshTurnPrompt: repairPrompt }),
3097
3136
  });
3137
+ // The repair turn resumes the run's session, which claude keys by the
3138
+ // directory that run started in.
3139
+ const repairCwd = mode === "resume" &&
3140
+ typeof lane.working_directory_at_spawn === "string" &&
3141
+ isExistingDirectory(lane.working_directory_at_spawn)
3142
+ ? lane.working_directory_at_spawn
3143
+ : hqRoot;
3098
3144
  const workerOutOffset = fileSizeOrZero(path.join(logsDir, "worker.out"));
3099
3145
  const previous = readSpawnManifest(runDir);
3100
3146
  let workerPid;
3101
3147
  try {
3102
3148
  noteCapacityAdvisory(lane, env);
3103
- workerPid = spawnTrackedWorker(spawnProcess, command.command, command.args, childEnv, hqRoot, path.join(logsDir, "worker.out"), path.join(logsDir, "worker.err"), path.join(logsDir, "worker-exit.json"));
3149
+ workerPid = spawnTrackedWorker(spawnProcess, command.command, command.args, childEnv, repairCwd, path.join(logsDir, "worker.out"), path.join(logsDir, "worker.err"), path.join(logsDir, "worker-exit.json"));
3104
3150
  writePidRecord(path.join(runDir, "worker.pid"), {
3105
3151
  pid: workerPid,
3106
3152
  starttime: readProcStarttime(workerPid) ?? undefined,
@@ -772,6 +772,11 @@ export interface LaneRecord {
772
772
  provider_session_id_absent?: string;
773
773
  /** Provider home used by the most recently spawned run, for transcript recovery. */
774
774
  provider_home_at_spawn?: string;
775
+ /**
776
+ * Working directory the most recently spawned run started in. Claude keys
777
+ * its transcript by this directory, so a later resume starts there too.
778
+ */
779
+ working_directory_at_spawn?: string;
775
780
  /** Set at spawn when this run continued a recorded session instead of starting fresh. */
776
781
  last_resume?: LaneResumeRecord;
777
782
  /** Last 50 lane worker starts after the initial start, including actor provenance. */
@@ -242,7 +242,7 @@ export const LANE_FIELD_WRITERS = {
242
242
  overdue: ["reconcile"], overdue_at: ["reconcile"], last_host_report_at: ["watcher", "spawn", "reconcile"], stalled: ["watcher"], stalled_at: ["watcher"], stalled_reason: ["watcher"],
243
243
  admitted_reason: ["command", "reconcile"], terminal_reason: ["command", "watcher", "spawn", "reconcile"], stop_history: ["command", "reconcile"], settings_history: ["command"], pending_settings: ["command", "spawn"], interrupted_at: ["command", "watcher", "reconcile", "spawn"], terminal_details: ["command", "spawn", "watcher", "reconcile"],
244
244
  quota_reset_at: ["watcher", "reconcile", "spawn", "command"], model_quota_marker: ["watcher", "reconcile"], outpost_command_id: ["command"],
245
- provider_session_id: ["watcher", "command", "spawn"], provider_session_id_absent: ["watcher", "command"], provider_home_at_spawn: ["spawn"], last_resume: ["spawn"], resume_history: ["spawn", "watcher"], last_run_usage: ["watcher"], last_run_usage_at: ["watcher"], envelope_repair: ["watcher", "reconcile"], envelope_validation_errors: ["watcher"], last_progress_event_at: ["watcher"],
245
+ provider_session_id: ["watcher", "command", "spawn"], provider_session_id_absent: ["watcher", "command"], provider_home_at_spawn: ["spawn"], working_directory_at_spawn: ["spawn"], last_resume: ["spawn"], resume_history: ["spawn", "watcher"], last_run_usage: ["watcher"], last_run_usage_at: ["watcher"], envelope_repair: ["watcher", "reconcile"], envelope_validation_errors: ["watcher"], last_progress_event_at: ["watcher"],
246
246
  };
247
247
  function isPlainObject(value) {
248
248
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -43,6 +43,7 @@ import { appendCappedHistory, listQuestionIds, readLane, readQuestion, retryUpda
43
43
  import { enforceRegistryApprovalGate } from "./approval-gate.js";
44
44
  import { readWorkerRunResult } from "./delivery.js";
45
45
  import { applyDefaultQuotaHold, clearAccountQuotaAfterSuccessfulTurnBestEffort, quotaSignalFromFields, readQuotaSignal, recordAccountQuota, } from "./quota.js";
46
+ import { deliverQuotaHoldNotice, quotaHoldNotice } from "./quota-notice.js";
46
47
  import { PROVIDER_REJECTED_REASON, QUOTA_STATUS, isProviderLoginFailure, providerRejectionDetails, readProviderRejection, } from "./provider-rejection.js";
47
48
  import { appendDropboxMessageAtPath, dropboxInboxHasMessages, readToolActivity, toolActivityPath } from "./dropbox.js";
48
49
  import { loadLanesConfig, providerProgressEventMinIntervalMs, providerQuotaDefaultHoldMs } from "./config.js";
@@ -1764,7 +1765,18 @@ async function runWatcherLoopOwned(hqRoot, laneId, deps) {
1764
1765
  (classification.reason === "account_quota_exhausted" ||
1765
1766
  classification.reason === "account_model_unavailable")) {
1766
1767
  try {
1767
- recordAccountQuota(hqRoot, quotaLane.provider, quotaLane.account, quota);
1768
+ const observedAt = new Date(now());
1769
+ const quotaState = recordAccountQuota(hqRoot, quotaLane.provider, quotaLane.account, quota, {
1770
+ now: observedAt,
1771
+ unconfirmedHoldMs: providerQuotaDefaultHoldMs(loadLanesConfig(hqRoot).providers[quotaLane.provider]),
1772
+ });
1773
+ if (classification.reason === "account_quota_exhausted") {
1774
+ const notice = quotaHoldNotice(quotaState, quotaLane.provider, quotaLane.account, quotaLane.lane_id);
1775
+ if (notice) {
1776
+ const deliveredTo = deliverQuotaHoldNotice(hqRoot, quotaLane, notice, observedAt.toISOString());
1777
+ logDecision({ event: "quota_hold_notice", at: tick.emit.at, account: quotaLane.account, delivered_to: deliveredTo });
1778
+ }
1779
+ }
1768
1780
  }
1769
1781
  catch (err) {
1770
1782
  process.stderr.write(`hq lanes watcher: quota record failed for ${laneId}: ${err instanceof Error ? err.message : String(err)}\n`);
@@ -137,6 +137,8 @@ export interface StoryPatch {
137
137
  status?: string;
138
138
  assigneeUid?: string;
139
139
  clearAssignee?: boolean;
140
+ /** Override another holder's assignment (server answers 409 otherwise). */
141
+ force?: boolean;
140
142
  }
141
143
  export interface StoryPatchResult {
142
144
  projectId: string;
@@ -56,6 +56,7 @@ export async function meshJson(token, path, init = {}) {
56
56
  ].filter(Boolean);
57
57
  const failure = new Error(`${res.status} ${code}${detail}${context.length ? ` (${context.join("; ")})` : ""}`);
58
58
  failure.status = res.status;
59
+ failure.body = data;
59
60
  if (typeof err.code === "string" && err.code)
60
61
  failure.code = err.code;
61
62
  throw failure;
@@ -398,6 +399,8 @@ export async function patchStory(token, companyUid, projectId, storyId, patch) {
398
399
  body.assigneeUid = patch.assigneeUid;
399
400
  if (patch.clearAssignee)
400
401
  body.clearAssignee = true;
402
+ if (patch.force)
403
+ body.force = true;
401
404
  const view = asRecord(await meshJson(token, `/v1/work-mesh/projects/${encodeURIComponent(projectId)}/stories/${encodeURIComponent(storyId)}`, {
402
405
  method: "PATCH",
403
406
  body,
@@ -114,6 +114,11 @@ export declare function fetchCompanyLive(token: string, companyUid: string, opts
114
114
  cursor?: string;
115
115
  get?: (path: string, query: Record<string, string>) => Promise<unknown>;
116
116
  }): Promise<CompanyLiveReadResponse>;
117
- export declare function formatCompanyLiveTable(live: CompanyLiveReadResponse, companyLabel: string): string[];
117
+ /**
118
+ * Fill participants[].displayName from the local directory cache when the
119
+ * server omitted it (older hq-pro builds do not send it).
120
+ */
121
+ export declare function withParticipantDisplayNames(live: CompanyLiveReadResponse, names: ReadonlyMap<string, string>): CompanyLiveReadResponse;
122
+ export declare function formatCompanyLiveTable(live: CompanyLiveReadResponse, companyLabel: string, names?: ReadonlyMap<string, string>): string[];
118
123
  export type { OrganizeSubmitDecision, OrganizeReceipt, CandidatesResponse, MigrateReceipt, PostMigrateFn, };
119
124
  //# sourceMappingURL=client.d.ts.map
@@ -290,22 +290,35 @@ export async function fetchCompanyLive(token, companyUid, opts) {
290
290
  : await meshJson(token, "/v1/work-mesh/live", { query });
291
291
  return data;
292
292
  }
293
- export function formatCompanyLiveTable(live, companyLabel) {
293
+ /**
294
+ * Fill participants[].displayName from the local directory cache when the
295
+ * server omitted it (older hq-pro builds do not send it).
296
+ */
297
+ export function withParticipantDisplayNames(live, names) {
298
+ return {
299
+ ...live,
300
+ participants: (live.participants ?? []).map((p) => {
301
+ const name = p.displayName?.trim() || names.get(p.actorUid);
302
+ return name ? { ...p, displayName: name } : p;
303
+ }),
304
+ };
305
+ }
306
+ export function formatCompanyLiveTable(live, companyLabel, names = new Map()) {
294
307
  const lines = [
295
308
  `Work mesh live: ${companyLabel} generatedAt=${live.generatedAt}`,
296
- "actorUid\tpresence\tdisplayName\tsessionId\tstatus\tcontext\tproject\ttask\tturns",
309
+ "name presence sessionId status context project task turns",
297
310
  ];
298
311
  for (const p of live.participants ?? []) {
312
+ const who = p.displayName?.trim() || names.get(p.actorUid) || p.actorUid;
299
313
  const sessions = p.sessions ?? [];
300
314
  if (sessions.length === 0) {
301
- lines.push(`${p.actorUid}\t${p.presence ?? "?"}\t${p.displayName ?? ""}\t-\t-\t-\t-\t-\t-`);
315
+ lines.push(`${who}\t${p.presence ?? "?"}\t-\t-\t-\t-\t-\t-`);
302
316
  continue;
303
317
  }
304
318
  for (const s of sessions) {
305
319
  lines.push([
306
- p.actorUid,
320
+ who,
307
321
  p.presence ?? "?",
308
- p.displayName ?? "",
309
322
  s.sessionId,
310
323
  s.status ?? "?",
311
324
  s.contextStatus ?? "?",
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Work Mesh team coordination (WMTC-7 / WMTC-8): story claims, overlap,
3
+ * project reassignment, assignee suggestions, and the presence roster.
4
+ *
5
+ * Every route here except the story PATCH is new on hq-pro. Callers treat a
6
+ * 404/405 without an error code as "this server does not have the route yet"
7
+ * and degrade to one plain line instead of failing.
8
+ */
9
+ export declare const STORY_ASSIGNEE_CONFLICT = "STORY_ASSIGNEE_CONFLICT";
10
+ /**
11
+ * True when the server does not know the route: 405, or a 404 that carries no
12
+ * error code. A coded 404 (for example PROJECT_NOT_FOUND) is a real answer.
13
+ */
14
+ export declare function isRouteUnavailable(err: unknown): boolean;
15
+ export declare function unavailableLine(feature: string): string;
16
+ export interface StoryAssigneeConflict {
17
+ code: typeof STORY_ASSIGNEE_CONFLICT;
18
+ error: string;
19
+ holderUid: string;
20
+ holderDisplayName?: string;
21
+ }
22
+ /** Extract a structured conflict from a 409 meshJson failure, if it is one. */
23
+ export declare function storyConflictFromError(err: unknown): StoryAssigneeConflict | null;
24
+ export declare function formatStoryConflictLine(storyId: string, conflict: StoryAssigneeConflict, names?: NameDirectory): string;
25
+ export type NameDirectory = Map<string, string>;
26
+ export interface DirectoryPerson {
27
+ uid: string;
28
+ displayName?: string;
29
+ email?: string;
30
+ }
31
+ export declare function peopleFromPayload(payload: unknown): DirectoryPerson[];
32
+ /** People known from the local contacts/directory cache. Never throws. */
33
+ export declare function loadCachedPeople(cacheRoot?: string): DirectoryPerson[];
34
+ export declare function loadNameDirectory(cacheRoot?: string): NameDirectory;
35
+ export declare function displayNameFor(uid: string, serverName: string | undefined, names: NameDirectory): string;
36
+ export type PersonResolution = {
37
+ ok: true;
38
+ uid: string;
39
+ displayName?: string;
40
+ } | {
41
+ ok: false;
42
+ reason: "none" | "ambiguous";
43
+ candidates: DirectoryPerson[];
44
+ };
45
+ /** Resolve a uid, email, or display name against a list of people. */
46
+ export declare function resolvePerson(query: string, people: DirectoryPerson[]): PersonResolution;
47
+ export interface MeshOverlap {
48
+ actorUid: string;
49
+ displayName?: string;
50
+ actorType?: string;
51
+ sessionId: string;
52
+ repo: string;
53
+ branch: string;
54
+ projectId?: string;
55
+ storyId?: string;
56
+ reasons: string[];
57
+ sharedFiles: string[];
58
+ lastSeenAt?: string;
59
+ }
60
+ export interface OverlapRequest {
61
+ companyUid: string;
62
+ sessionId: string;
63
+ repo: string;
64
+ branch: string;
65
+ projectId?: string;
66
+ storyId?: string;
67
+ files?: string[];
68
+ }
69
+ export declare const OVERLAP_MAX_FILES = 200;
70
+ export declare function postOverlap(token: string, req: OverlapRequest): Promise<MeshOverlap[]>;
71
+ export declare function formatOverlapLines(overlaps: MeshOverlap[], names: NameDirectory): string[];
72
+ export interface ReassignResult {
73
+ projectId: string;
74
+ ownerUid: string;
75
+ previousOwnerUid: string | null;
76
+ reassignedStories: string[];
77
+ }
78
+ export declare function postProjectReassign(token: string, projectId: string, body: {
79
+ companyUid: string;
80
+ toUid: string;
81
+ includeOpenStories?: boolean;
82
+ note?: string;
83
+ }): Promise<ReassignResult>;
84
+ export interface SuggestionCandidate {
85
+ actorUid: string;
86
+ displayName?: string;
87
+ actorType?: string;
88
+ score: number;
89
+ idle: boolean;
90
+ reasons: string[];
91
+ }
92
+ export declare function fetchStorySuggestions(token: string, companyUid: string, projectId: string, storyId: string): Promise<SuggestionCandidate[]>;
93
+ export declare function formatSuggestionLines(storyId: string, candidates: SuggestionCandidate[], names: NameDirectory): string[];
94
+ export interface RosterMember {
95
+ actorUid: string;
96
+ displayName?: string;
97
+ actorType?: string;
98
+ daemonLastSeenAt: string | null;
99
+ lastSessionAt: string | null;
100
+ boundSessions: number;
101
+ unboundSessions: number;
102
+ }
103
+ export declare function fetchRoster(token: string, companyUid: string): Promise<RosterMember[]>;
104
+ export declare function rosterFlags(member: RosterMember): string[];
105
+ export declare function formatRosterLines(members: RosterMember[], companyLabel: string, names: NameDirectory): string[];
106
+ //# sourceMappingURL=team-coordination.d.ts.map