@indigoai-us/hq-cli 5.345.146 → 5.345.148

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 (138) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +14 -0
  3. package/assets/bot-workers/setup/worker.yaml +11 -0
  4. package/dist/command-catalog.generated.d.ts +326 -4
  5. package/dist/command-catalog.generated.js +403 -4
  6. package/dist/commands/agent-inbox.js +2 -1
  7. package/dist/commands/auth.js +4 -2
  8. package/dist/commands/bot.js +1 -1
  9. package/dist/commands/cloud.js +31 -1
  10. package/dist/commands/core-refresh-vault-access-native.d.ts +17 -0
  11. package/dist/commands/core-refresh-vault-access-native.js +214 -19
  12. package/dist/commands/core-work-mesh-bind-trusted-native.d.ts +9 -0
  13. package/dist/commands/core-work-mesh-bind-trusted-native.js +26 -19
  14. package/dist/commands/core.js +6 -0
  15. package/dist/commands/daemon.d.ts +7 -0
  16. package/dist/commands/daemon.js +37 -3
  17. package/dist/commands/dm.js +9 -2
  18. package/dist/commands/doctor.d.ts +11 -1
  19. package/dist/commands/doctor.js +56 -1
  20. package/dist/commands/files.js +10 -6
  21. package/dist/commands/install-global.d.ts +7 -0
  22. package/dist/commands/install-global.js +129 -35
  23. package/dist/commands/lanes-shared.d.ts +3 -1
  24. package/dist/commands/lanes.d.ts +4 -4
  25. package/dist/commands/lanes.js +380 -25
  26. package/dist/commands/login.js +4 -3
  27. package/dist/commands/pack-install.d.ts +2 -2
  28. package/dist/commands/pack-install.js +133 -35
  29. package/dist/commands/sync-manifest.d.ts +2 -9
  30. package/dist/commands/sync-manifest.js +4 -15
  31. package/dist/hook-engine-path.d.ts +4 -0
  32. package/dist/hook-engine-path.js +36 -0
  33. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2784 -2360
  34. package/dist/hq-cloud-sync-runner/package.json +1 -1
  35. package/dist/index.js +6 -0
  36. package/dist/lib/agent-kit/inbox-redact.d.ts +44 -0
  37. package/dist/lib/agent-kit/inbox-redact.js +361 -0
  38. package/dist/lib/agent-kit/inbox-state.js +11 -2
  39. package/dist/lib/agent-kit/mcp/tools.js +2 -1
  40. package/dist/lib/agent-kit/run/inbox.js +36 -7
  41. package/dist/lib/anywhere-runtime-flag-key.d.ts +3 -0
  42. package/dist/lib/anywhere-runtime-flag-key.js +9 -0
  43. package/dist/lib/bot/daemon.js +3 -0
  44. package/dist/lib/bot/runtime/index.js +3 -1
  45. package/dist/lib/company-skill-wrapper.js +6 -4
  46. package/dist/lib/daemon/install.d.ts +47 -1
  47. package/dist/lib/daemon/install.js +181 -15
  48. package/dist/lib/daemon/lock.d.ts +32 -0
  49. package/dist/lib/daemon/lock.js +55 -2
  50. package/dist/lib/doctor/__testing__/run-doctor.js +4 -0
  51. package/dist/lib/doctor/checks/anywhere.d.ts +3 -0
  52. package/dist/lib/doctor/checks/anywhere.js +23 -8
  53. package/dist/lib/doctor/checks/cli-path.d.ts +11 -0
  54. package/dist/lib/doctor/checks/cli-path.js +45 -0
  55. package/dist/lib/doctor/checks/hook-engine.d.ts +4 -0
  56. package/dist/lib/doctor/checks/hook-engine.js +16 -0
  57. package/dist/lib/doctor/checks/release-links.d.ts +23 -0
  58. package/dist/lib/doctor/checks/release-links.js +152 -0
  59. package/dist/lib/doctor/checks/sync-health.d.ts +7 -0
  60. package/dist/lib/doctor/checks/sync-health.js +38 -2
  61. package/dist/lib/doctor/checks/temp-homes.d.ts +19 -0
  62. package/dist/lib/doctor/checks/temp-homes.js +54 -0
  63. package/dist/lib/doctor/fix/apply.d.ts +90 -1
  64. package/dist/lib/doctor/fix/apply.js +176 -10
  65. package/dist/lib/doctor/fix/json-output.d.ts +4 -1
  66. package/dist/lib/doctor/fix/json-output.js +6 -0
  67. package/dist/lib/doctor/fix/remediation.d.ts +7 -1
  68. package/dist/lib/doctor/fix/remediation.js +23 -1
  69. package/dist/lib/doctor/registry.js +13 -0
  70. package/dist/lib/doctor/temp-home-sweeper.d.ts +149 -0
  71. package/dist/lib/doctor/temp-home-sweeper.js +418 -0
  72. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  73. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  74. package/dist/lib/flag-registry.js +9 -1
  75. package/dist/lib/flag-snapshot-file.js +8 -3
  76. package/dist/lib/hook-engine/resolve.d.ts +31 -0
  77. package/dist/lib/hook-engine/resolve.js +134 -0
  78. package/dist/lib/lanes/admission-host.d.ts +1 -0
  79. package/dist/lib/lanes/admission-host.js +8 -1
  80. package/dist/lib/lanes/admission.d.ts +4 -0
  81. package/dist/lib/lanes/admission.js +6 -2
  82. package/dist/lib/lanes/envelope-repair-dispatch.d.ts +5 -0
  83. package/dist/lib/lanes/envelope-repair-dispatch.js +12 -1
  84. package/dist/lib/lanes/index.d.ts +2 -2
  85. package/dist/lib/lanes/index.js +1 -1
  86. package/dist/lib/lanes/keep-alive.js +4 -1
  87. package/dist/lib/lanes/local-host-identity.d.ts +40 -0
  88. package/dist/lib/lanes/local-host-identity.js +314 -0
  89. package/dist/lib/lanes/reaper.d.ts +1 -1
  90. package/dist/lib/lanes/reaper.js +7 -0
  91. package/dist/lib/lanes/reconcile.d.ts +4 -0
  92. package/dist/lib/lanes/reconcile.js +16 -8
  93. package/dist/lib/lanes/spawn.d.ts +20 -0
  94. package/dist/lib/lanes/spawn.js +48 -2
  95. package/dist/lib/lanes/status-fields.js +6 -1
  96. package/dist/lib/lanes/store.js +8 -1
  97. package/dist/lib/lanes/throttle.d.ts +49 -6
  98. package/dist/lib/lanes/throttle.js +154 -17
  99. package/dist/lib/lanes/types.d.ts +22 -1
  100. package/dist/lib/lanes/types.js +27 -3
  101. package/dist/lib/lanes/watcher.js +27 -2
  102. package/dist/lib/lanes/worker-continuity.d.ts +2 -0
  103. package/dist/lib/lanes/worker-continuity.js +18 -11
  104. package/dist/lib/manifest-disabled.d.ts +13 -0
  105. package/dist/lib/manifest-disabled.js +19 -0
  106. package/dist/lib/service-manager/launchd.d.ts +1 -0
  107. package/dist/lib/service-manager/launchd.js +1 -1
  108. package/dist/lib/windows/user-path.d.ts +48 -0
  109. package/dist/lib/windows/user-path.js +114 -0
  110. package/dist/lib/work-context/auto-bind.d.ts +1 -1
  111. package/dist/lib/work-context/auto-bind.js +22 -3
  112. package/dist/lib/work-context/lane-bind.d.ts +31 -0
  113. package/dist/lib/work-context/lane-bind.js +101 -0
  114. package/dist/lib/work-context/organize.js +1 -0
  115. package/dist/lib/work-context/prd-sync.js +7 -0
  116. package/dist/lib/work-context/project.d.ts +36 -10
  117. package/dist/lib/work-context/project.js +130 -41
  118. package/dist/lib/work-context/reconcile.d.ts +6 -0
  119. package/dist/lib/work-context/reconcile.js +25 -5
  120. package/dist/lib/work-context/state.d.ts +6 -0
  121. package/dist/lib/work-context/state.js +29 -1
  122. package/dist/lib/workers-registry/index.d.ts +6 -0
  123. package/dist/lib/workers-registry/index.js +8 -0
  124. package/dist/utils/copy-tree.d.ts +61 -0
  125. package/dist/utils/copy-tree.js +249 -0
  126. package/dist/utils/environmental-error.js +7 -3
  127. package/dist/utils/headless.d.ts +2 -0
  128. package/dist/utils/headless.js +8 -0
  129. package/dist/utils/login-browser.d.ts +4 -0
  130. package/dist/utils/login-browser.js +122 -0
  131. package/dist/utils/npm-invocation.d.ts +27 -0
  132. package/dist/utils/npm-invocation.js +54 -0
  133. package/dist/utils/safe-link.d.ts +50 -0
  134. package/dist/utils/safe-link.js +300 -0
  135. package/dist/utils/team-upgrade.js +1 -7
  136. package/package.json +17 -5
  137. package/scripts/hook-engine-manifest.mjs +22 -0
  138. package/scripts/install-hook-engine.mjs +10 -0
@@ -2,16 +2,24 @@
2
2
  * Project/task resolution for work-context (US-007B).
3
3
  *
4
4
  * Order:
5
- * 1. existing active binding (state bound / needs_task with projectId)
6
- * 2. trusted explicit (HQ_SPAWN_PROJECT/HQ_SPAWN_TASK, observation, meta.yaml)
7
- * 3. exactly one deterministic mapping (cwd under companies/{slug}/projects/{project}/
8
- * or a registry resource that names exactly one project for the repo)
5
+ * 1. explicit trusted bind (observation authority "trusted"; shell alias
6
+ * `project` / `task`). Beats an existing automatic binding and is not
7
+ * refused because another session owns a lane. Refused when the project
8
+ * directory is not under the resolved company.
9
+ * 2. existing active binding (state bound / needs_task with projectId).
10
+ * Ambient HQ_SPAWN_PROJECT and meta.yaml do not override it.
11
+ * 3. trusted explicit (HQ_SPAWN_PROJECT/HQ_SPAWN_TASK, observation projectId,
12
+ * meta.yaml). Meta and the bare `project` alias are refused when another
13
+ * session owns an active lane for that project. Spawn is not.
14
+ * 4. exactly one deterministic mapping (cwd or registry), refused when
15
+ * another session owns an active lane for that project.
9
16
  * Soft evidence never auto-binds — candidates are server-owned (GET .../candidates).
10
17
  */
11
18
  import * as fs from "node:fs";
12
19
  import * as path from "node:path";
13
20
  import { projectIdFromCwd } from "./company.js";
14
21
  import { normalizeTaskId } from "./contract.js";
22
+ import { foreignSessionOwnsLane } from "./lane-bind.js";
15
23
  import { findEnclosingGitDir, normalizeRemoteOwnerName, readOriginRemoteUrl, } from "./repo-remote.js";
16
24
  const SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
17
25
  function normalizeSlug(value) {
@@ -120,6 +128,55 @@ export function projectIdFromRepoRegistry(input) {
120
128
  return [...matches][0];
121
129
  return null;
122
130
  }
131
+ function readResolveMeta(input) {
132
+ if (input.metaProjectId !== undefined || input.metaTaskId !== undefined) {
133
+ return {
134
+ projectId: normalizeSlug(input.metaProjectId) ?? undefined,
135
+ taskId: normalizeSlug(input.metaTaskId) ?? undefined,
136
+ };
137
+ }
138
+ return readMetaProjectTask(input.sessionId, input.hqRoot);
139
+ }
140
+ function trustedTaskFrom(trusted, spawnTask) {
141
+ if (trusted?.taskId || trusted?.storyId) {
142
+ const norm = normalizeTaskId({
143
+ taskId: trusted.taskId,
144
+ storyId: trusted.storyId,
145
+ });
146
+ if (norm.ok)
147
+ return norm.taskId;
148
+ }
149
+ // Spawn wins over the bare `task` alias, matching the pre-authority order.
150
+ return spawnTask ?? normalizeSlug(trusted?.task);
151
+ }
152
+ /**
153
+ * A trusted project belongs to the resolved company only when that company's
154
+ * project directory exists. Missing company or root fails closed.
155
+ */
156
+ function trustedProjectInResolvedCompany(input, projectId) {
157
+ const companySlug = input.resolvedCompanySlug?.trim();
158
+ const hqRoot = input.hqRoot?.trim();
159
+ if (!companySlug || !hqRoot)
160
+ return false;
161
+ try {
162
+ return fs.statSync(path.join(hqRoot, "companies", companySlug, "projects", projectId)).isDirectory();
163
+ }
164
+ catch {
165
+ return false;
166
+ }
167
+ }
168
+ /** Meta, cwd, and registry must not attach a project another session owns. */
169
+ function automaticProjectBlocked(input, projectId) {
170
+ const companySlug = input.resolvedCompanySlug?.trim()
171
+ || input.existingState?.companySlug?.trim();
172
+ return foreignSessionOwnsLane({
173
+ hqRoot: input.hqRoot,
174
+ projectId,
175
+ sessionId: input.sessionId,
176
+ companySlug,
177
+ laneCache: input.laneCache,
178
+ });
179
+ }
123
180
  /**
124
181
  * Resolve project and task. Never invents an adhoc target. Soft evidence is
125
182
  * out of scope here (server candidates only).
@@ -127,7 +184,30 @@ export function projectIdFromRepoRegistry(input) {
127
184
  export function resolveProjectTask(input) {
128
185
  const env = input.env ?? process.env;
129
186
  const state = input.existingState ?? null;
130
- // 1. Existing active binding.
187
+ const trusted = input.trusted;
188
+ const spawnTask = normalizeSlug(env.HQ_SPAWN_TASK);
189
+ // 1. Explicit trusted bind. The shell writes `project`/`task` plus
190
+ // authority "trusted". That beats an existing automatic binding. Do not
191
+ // keep the previous project's task when the project changes.
192
+ const explicitTrusted = trusted?.authority === "trusted"
193
+ ? normalizeSlug(trusted.projectId ?? trusted.project)
194
+ : undefined;
195
+ if (explicitTrusted && trustedProjectInResolvedCompany(input, explicitTrusted)) {
196
+ const meta = readResolveMeta(input);
197
+ let taskId = trustedTaskFrom(trusted, undefined);
198
+ if (!taskId && state?.projectId === explicitTrusted)
199
+ taskId = state.taskId;
200
+ if (!taskId && meta.projectId === explicitTrusted)
201
+ taskId = meta.taskId;
202
+ return {
203
+ projectId: explicitTrusted,
204
+ taskId,
205
+ source: "trusted_explicit",
206
+ complete: Boolean(explicitTrusted && taskId),
207
+ authority: "trusted",
208
+ };
209
+ }
210
+ // 2. Existing active binding. Ambient spawn and meta do not override it.
131
211
  if (state) {
132
212
  const boundStatuses = new Set(["bound", "needs_task", "needs_project"]);
133
213
  if (boundStatuses.has(state.contextStatus) &&
@@ -154,40 +234,49 @@ export function resolveProjectTask(input) {
154
234
  };
155
235
  }
156
236
  }
157
- // 2. Trusted explicit: env + observation + meta.yaml.
237
+ // 3. Trusted explicit: spawn + canonical observation projectId + meta.
238
+ // The bare `project` alias without authority is meta evidence.
158
239
  const spawnProject = normalizeSlug(env.HQ_SPAWN_PROJECT);
159
- const spawnTask = normalizeSlug(env.HQ_SPAWN_TASK);
160
- const trustedProject = normalizeSlug(input.trusted?.projectId ?? spawnProject);
161
- let trustedTask = spawnTask;
162
- if (input.trusted?.taskId || input.trusted?.storyId) {
163
- const norm = normalizeTaskId({
164
- taskId: input.trusted.taskId,
165
- storyId: input.trusted.storyId,
166
- });
167
- if (norm.ok)
168
- trustedTask = norm.taskId;
169
- }
170
- else if (spawnTask) {
171
- trustedTask = spawnTask;
240
+ // A refused trusted project must not come back through the same observation's
241
+ // project alias or canonical projectId at a lower tier.
242
+ const canonicalTrustedProject = explicitTrusted
243
+ ? undefined
244
+ : normalizeSlug(trusted?.projectId);
245
+ const aliasProject = explicitTrusted
246
+ ? undefined
247
+ : normalizeSlug(trusted?.project);
248
+ // The refused observation's task is dropped with its project.
249
+ const trustedTask = explicitTrusted
250
+ ? spawnTask
251
+ : trustedTaskFrom(trusted, spawnTask);
252
+ const resolvedMeta = readResolveMeta(input);
253
+ // The bind script writes meta before reconcile, so a refused trusted bind
254
+ // also left its project in meta. When a trusted bind was refused, meta must
255
+ // pass the same company check before it can attach.
256
+ const meta = explicitTrusted
257
+ && resolvedMeta.projectId
258
+ && !trustedProjectInResolvedCompany(input, resolvedMeta.projectId)
259
+ ? {}
260
+ : resolvedMeta;
261
+ const explicitProject = canonicalTrustedProject ?? spawnProject;
262
+ if (explicitProject) {
263
+ const taskId = trustedTask ?? meta.taskId;
264
+ return {
265
+ projectId: explicitProject,
266
+ taskId,
267
+ source: "trusted_explicit",
268
+ complete: Boolean(explicitProject && taskId),
269
+ };
172
270
  }
173
- const meta = input.metaProjectId !== undefined || input.metaTaskId !== undefined
174
- ? {
175
- projectId: normalizeSlug(input.metaProjectId) ?? undefined,
176
- taskId: normalizeSlug(input.metaTaskId) ?? undefined,
177
- }
178
- : readMetaProjectTask(input.sessionId, input.hqRoot);
179
- const explicitProject = trustedProject ?? meta.projectId;
180
- const explicitTask = trustedTask ?? meta.taskId;
181
- if (explicitProject || explicitTask) {
182
- // Prefer spawn/trusted over meta; already merged above.
183
- if (explicitProject) {
184
- return {
185
- projectId: explicitProject,
186
- taskId: explicitTask,
187
- source: "trusted_explicit",
188
- complete: Boolean(explicitProject && explicitTask),
189
- };
190
- }
271
+ const metaProject = meta.projectId ?? aliasProject;
272
+ if (metaProject && !automaticProjectBlocked(input, metaProject)) {
273
+ const taskId = trustedTask ?? meta.taskId;
274
+ return {
275
+ projectId: metaProject,
276
+ taskId,
277
+ source: "trusted_explicit",
278
+ complete: Boolean(metaProject && taskId),
279
+ };
191
280
  }
192
281
  // Preserve existing project from needs_task without task (not a full bind).
193
282
  if (state?.projectId && state.contextStatus === "needs_task") {
@@ -198,9 +287,9 @@ export function resolveProjectTask(input) {
198
287
  complete: Boolean(state.taskId),
199
288
  };
200
289
  }
201
- // 3. Exactly one deterministic mapping.
290
+ // 4. Exactly one deterministic mapping, unless another session owns it.
202
291
  const fromCwd = projectIdFromCwd(input.cwd, input.hqRoot);
203
- if (fromCwd) {
292
+ if (fromCwd && !automaticProjectBlocked(input, fromCwd)) {
204
293
  return {
205
294
  projectId: fromCwd,
206
295
  source: "deterministic_cwd",
@@ -213,10 +302,10 @@ export function resolveProjectTask(input) {
213
302
  cwd: input.cwd,
214
303
  hqRoot: input.hqRoot,
215
304
  companySlug: state?.companySlug ??
216
- input.trusted?.companySlug ??
305
+ trusted?.companySlug ??
217
306
  normalizeSlug(env.HQ_SPAWN_COMPANY),
218
307
  });
219
- if (fromRegistry) {
308
+ if (fromRegistry && !automaticProjectBlocked(input, fromRegistry)) {
220
309
  return {
221
310
  projectId: fromRegistry,
222
311
  source: "deterministic_registry",
@@ -18,10 +18,16 @@ export interface ReconcileObservation {
18
18
  clientOperationId: string;
19
19
  explicit?: TrustedExplicitContext & {
20
20
  companySlug?: string;
21
+ project?: string;
22
+ task?: string;
23
+ authority?: string;
21
24
  };
22
25
  /** Alias for explicit (dispatch envelope). */
23
26
  trustedContext?: TrustedExplicitContext & {
24
27
  companySlug?: string;
28
+ project?: string;
29
+ task?: string;
30
+ authority?: string;
25
31
  };
26
32
  untrackedOrigin?: "user";
27
33
  /** Structured untracked choice; only origin "user" is accepted. */
@@ -11,6 +11,7 @@ import { WORK_CONTEXT_CONTRACT_VERSION, normalizeTaskId, } from "./contract.js";
11
11
  import { EXIT_INVALID_IDENTITY, EXIT_NOT_TRACKING, EXIT_OK, InvalidDecisionOriginError, NotTrackingError, } from "./errors.js";
12
12
  import { decisionFromCandidates, } from "./organize.js";
13
13
  import { refineProjectBinding } from "./bind-best.js";
14
+ import { foreignSessionOwnsLane } from "./lane-bind.js";
14
15
  import { rebindNeedsProjectOnPrompt } from "./prompt-rebind.js";
15
16
  import { resolveProjectTask, shouldAskAfter } from "./project.js";
16
17
  import { deriveRemoteOwnerSlug, deriveRepoIdentityKey } from "./repo-remote.js";
@@ -502,6 +503,8 @@ export async function reconcileObservation(obs, deps) {
502
503
  contextStatus: "needs_project",
503
504
  }
504
505
  : prior;
506
+ // Every lane check in this reconcile reads the lane files once.
507
+ const laneCache = new Map();
505
508
  let projectResolved = resolveProjectTask({
506
509
  sessionId,
507
510
  env,
@@ -509,6 +512,8 @@ export async function reconcileObservation(obs, deps) {
509
512
  cwd,
510
513
  hqRoot,
511
514
  existingState: projectPrior,
515
+ resolvedCompanySlug: resolvedCompany.slug,
516
+ laneCache,
512
517
  });
513
518
  // Deterministic cwd projects belong to the cwd company. A higher-precedence
514
519
  // identity UID must not adopt a project from an unrelated/unverified slug.
@@ -525,6 +530,7 @@ export async function reconcileObservation(obs, deps) {
525
530
  let projectGate;
526
531
  if (projectResolved &&
527
532
  projectId &&
533
+ projectResolved.authority !== "trusted" &&
528
534
  projectResolved.source !== "existing_binding" &&
529
535
  obs.source !== "transcript" &&
530
536
  hqRoot &&
@@ -539,11 +545,23 @@ export async function reconcileObservation(obs, deps) {
539
545
  nowMs: nowFn().getTime(),
540
546
  });
541
547
  if (refined.action === "bind") {
542
- if (refined.projectId !== projectId)
543
- taskId = undefined;
544
- projectId = refined.projectId;
545
- projectBindReason = refined.reason;
546
- diagnostics.push(`project bind: ${refined.reason}`);
548
+ const switching = refined.projectId !== projectId;
549
+ const foreignSwitch = switching && foreignSessionOwnsLane({
550
+ hqRoot,
551
+ projectId: refined.projectId,
552
+ sessionId,
553
+ companySlug: resolvedCompany.slug,
554
+ laneCache,
555
+ });
556
+ // Confirming the already-allowed hint is fine. Do not follow a refine
557
+ // switch onto a lane another session owns.
558
+ if (!foreignSwitch) {
559
+ if (switching)
560
+ taskId = undefined;
561
+ projectId = refined.projectId;
562
+ projectBindReason = refined.reason;
563
+ diagnostics.push(`project bind: ${refined.reason}`);
564
+ }
547
565
  }
548
566
  else if (refined.action === "create") {
549
567
  projectBindReason = refined.reason;
@@ -850,6 +868,7 @@ export async function reconcileObservation(obs, deps) {
850
868
  ...(decision ? { decision } : {}),
851
869
  ...(companyConfidence ? { companyConfidence } : {}),
852
870
  ...(presenceLeave ? { presenceLeave } : {}),
871
+ ...(projectResolved?.authority === "trusted" ? { projectBindAuthority: "trusted" } : {}),
853
872
  ...bindStateFields,
854
873
  ...transcriptFields,
855
874
  }, prior, companyRebind
@@ -936,6 +955,7 @@ export async function reconcileObservation(obs, deps) {
936
955
  startedAt,
937
956
  ...(typeof toolWrites === "number" ? { toolWrites } : {}),
938
957
  ...(hasTurn ? { hasTurn: true } : {}),
958
+ ...(projectResolved?.authority === "trusted" ? { projectBindAuthority: "trusted" } : {}),
939
959
  ...bindStateFields,
940
960
  ...transcriptFields,
941
961
  },
@@ -93,6 +93,12 @@ export interface SessionStateFile {
93
93
  companyConfidence?: "device_default";
94
94
  /** Why the current project was chosen, or why the session is asking. */
95
95
  projectBindReason?: string;
96
+ /**
97
+ * LOCAL-ONLY. "trusted" means an explicit trusted bind, operator
98
+ * bind-project, or organize receipt chose this project. Automatic rebind
99
+ * must not replace it until the next explicit choice.
100
+ */
101
+ projectBindAuthority?: "trusted";
96
102
  /** Ranked alternatives when the session is waiting for a project choice. */
97
103
  projectCandidates?: Array<{
98
104
  projectId: string;
@@ -207,6 +207,8 @@ function projectSessionState(state) {
207
207
  out.companyConfidence = state.companyConfidence;
208
208
  if (state.projectBindReason)
209
209
  out.projectBindReason = state.projectBindReason;
210
+ if (state.projectBindAuthority === "trusted")
211
+ out.projectBindAuthority = "trusted";
210
212
  if (state.projectCandidates?.length)
211
213
  out.projectCandidates = state.projectCandidates;
212
214
  if (state.projectGate)
@@ -299,6 +301,18 @@ export function mergeLocalSessionFields(next, prior, options = {}) {
299
301
  sameBinding) {
300
302
  out.bindingEpisodeId = prior.bindingEpisodeId;
301
303
  }
304
+ // A later hook that rewrites the same project in the same company must not
305
+ // drop an explicit trusted lock. A company change clears it even when the
306
+ // project slug matches. A different project keeps the lock only when the
307
+ // caller sets it again. A hook that omits company still counts as the same
308
+ // company, because preserveCompanyBinding copied the prior company above.
309
+ if (sameCompanyBinding &&
310
+ out.projectBindAuthority !== "trusted" &&
311
+ prior.projectBindAuthority === "trusted" &&
312
+ prior.projectId &&
313
+ out.projectId === prior.projectId) {
314
+ out.projectBindAuthority = "trusted";
315
+ }
302
316
  return out;
303
317
  }
304
318
  function acknowledgedStatus(binding) {
@@ -397,7 +411,20 @@ export function writeAcknowledgedRegisterBinding(binding, root, now = () => new
397
411
  }
398
412
  const acknowledgementStrength = acknowledged === "bound" ? 3 : acknowledged === "needs_task" ? 2 : 1;
399
413
  const priorStrength = bindingStrength(prior);
400
- const preservePriorBinding = Boolean(prior.companyUid === binding.companyUid && priorStrength > acknowledgementStrength);
414
+ // Acked register ops are never pruned, and flush replays them on every
415
+ // cycle. An older ack for another project in the same company must not
416
+ // undo an explicit trusted bind, whatever its strength.
417
+ const sameCompanyAsPrior = prior.companyUid === binding.companyUid ||
418
+ Boolean(!prior.companyUid &&
419
+ prior.companySlug &&
420
+ binding.companySlug &&
421
+ prior.companySlug.toLowerCase() === binding.companySlug.toLowerCase());
422
+ const keepTrustedPrior = Boolean(sameCompanyAsPrior &&
423
+ prior.projectBindAuthority === "trusted" &&
424
+ prior.projectId &&
425
+ binding.projectId !== prior.projectId);
426
+ const preservePriorBinding = Boolean(keepTrustedPrior ||
427
+ (prior.companyUid === binding.companyUid && priorStrength > acknowledgementStrength));
401
428
  const sameBindingAsPrior = prior.companyUid === binding.companyUid &&
402
429
  prior.projectId === binding.projectId &&
403
430
  prior.taskId === binding.taskId;
@@ -474,6 +501,7 @@ export function applyOperatorProjectBinding(state, projectId, now = () => new Da
474
501
  projectGate: undefined,
475
502
  projectCandidates: undefined,
476
503
  projectBindReason: `operator selected ${projectId}`,
504
+ projectBindAuthority: "trusted",
477
505
  updatedAt: now().toISOString(),
478
506
  };
479
507
  }
@@ -23,6 +23,12 @@ export interface GenerateWorkersRegistryResult {
23
23
  * own, and because duplicates resolve by path order — a tenant path sorts before
24
24
  * both `core` and `personal` — the checkout wins. The operator's actual worker
25
25
  * then silently vanishes from the registry every skill reads.
26
+ *
27
+ * Claude Code worktrees (`.claude/worktrees/<name>/`) and Codex worktrees
28
+ * (`.codex/worktrees/<name>/`) are the same trap in a different folder: a
29
+ * nested HQ root under a company project. Those copies land in the registry as
30
+ * `companies/<slug>/…/.claude/worktrees/…` or `.codex/worktrees/…` paths, so
31
+ * `hq lanes create` then fails with `worker_tenancy_unresolved`.
26
32
  */
27
33
  export declare function isExcludedWorkerDirectory(relativeDirectory: string): boolean;
28
34
  /**
@@ -157,9 +157,17 @@ function withoutTimestamp(content) {
157
157
  * own, and because duplicates resolve by path order — a tenant path sorts before
158
158
  * both `core` and `personal` — the checkout wins. The operator's actual worker
159
159
  * then silently vanishes from the registry every skill reads.
160
+ *
161
+ * Claude Code worktrees (`.claude/worktrees/<name>/`) and Codex worktrees
162
+ * (`.codex/worktrees/<name>/`) are the same trap in a different folder: a
163
+ * nested HQ root under a company project. Those copies land in the registry as
164
+ * `companies/<slug>/…/.claude/worktrees/…` or `.codex/worktrees/…` paths, so
165
+ * `hq lanes create` then fails with `worker_tenancy_unresolved`.
160
166
  */
161
167
  export function isExcludedWorkerDirectory(relativeDirectory) {
162
168
  const normalized = relativeDirectory.replaceAll('\\', '/');
169
+ if (/(?:^|\/)\.(?:claude|codex)\/worktrees(?:\/|$)/.test(normalized))
170
+ return true;
163
171
  const [root, tenant, nested] = normalized.split('/');
164
172
  if (root !== 'companies')
165
173
  return /(?:^|\/)_overrides(?:\/|$)/.test(normalized);
@@ -0,0 +1,61 @@
1
+ /**
2
+ * copy-tree: portable replacement for the `rsync -a` copies on the pack
3
+ * install path.
4
+ *
5
+ * Pack installs used to shell out to `rsync`, which Windows does not ship, so
6
+ * every git-subpath or local-directory install failed there with
7
+ * `spawnSync rsync ENOENT`. This module does the same copy in Node on every
8
+ * platform:
9
+ *
10
+ * - `copyTree(src, dest)` copies the CONTENTS of `src` into `dest` (the
11
+ * `rsync -a src/ dest/` trailing-slash form). Directories recurse, regular
12
+ * files keep their permission bits and mtimes, and symlinks are recreated
13
+ * as symlinks with the identical target string. Links are never followed.
14
+ * Optional `exclude` names match an entry's basename at any depth, like
15
+ * `rsync --exclude=<name>`. Sockets, FIFOs and devices are skipped with a
16
+ * warning, as a non-root `rsync -a` does.
17
+ * - `removeTreeNoFollow(target)` deletes a tree without ever following a
18
+ * symlink or junction inside it: each entry is lstat'd, links are removed
19
+ * as links (unlink, or a non-recursive rmdir for a Windows junction), and
20
+ * directories are emptied then removed with a non-recursive rmdir. A
21
+ * Windows directory that is a reparse point but not a link (a volume mount
22
+ * point, a cloud-synced placeholder folder) stops the removal with
23
+ * `ReparsePointError`; its contents are never deleted.
24
+ *
25
+ * On Windows, creating a symlink needs Developer Mode or an elevated shell.
26
+ * When that fails with EPERM/EACCES and the link points at a directory inside
27
+ * the tree being copied, the copy falls back to a junction (no privilege
28
+ * needed). A junction needs an absolute target, so it points at the matching
29
+ * entry under `finalRoot`: the path the copy will live at once the caller has
30
+ * renamed it into place. Any other link target throws `CopyTreeLinkError` with
31
+ * a Developer Mode hint instead of silently dropping the link.
32
+ *
33
+ * TODO(windows-links): src/utils/safe-link.ts (PR 1967, branch
34
+ * fix/windows-install-path-daemon) adds its own junction fallback and
35
+ * link-only removal. Whichever of the two PRs merges second should reconcile
36
+ * these helpers into one module.
37
+ */
38
+ export interface CopyTreeOptions {
39
+ /** Entry basenames to skip at any depth (`rsync --exclude=<name>`). */
40
+ exclude?: readonly string[];
41
+ /**
42
+ * Where `dest` will end up after the caller renames it (for example a
43
+ * staging dir that is renamed into `core/packages/<name>`). Junction
44
+ * fallbacks point under this path so they still resolve after the rename.
45
+ * Defaults to `dest`.
46
+ */
47
+ finalRoot?: string;
48
+ }
49
+ export declare class CopyTreeLinkError extends Error {
50
+ constructor(linkPath: string, target: string, cause: NodeJS.ErrnoException);
51
+ }
52
+ export declare class ReparsePointError extends Error {
53
+ constructor(dir: string);
54
+ }
55
+ export declare function copyTree(src: string, dest: string, opts?: CopyTreeOptions): void;
56
+ /**
57
+ * Remove `target` and everything under it without following links. A missing
58
+ * target is not an error. If `target` itself is a link, only the link goes.
59
+ */
60
+ export declare function removeTreeNoFollow(target: string): void;
61
+ //# sourceMappingURL=copy-tree.d.ts.map