@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
@@ -0,0 +1,48 @@
1
+ export type RegRun = (cmd: string, args: string[]) => {
2
+ status: number | null;
3
+ stdout: string;
4
+ };
5
+ export interface ShimPathCheck {
6
+ /** The folder holding `hq.cmd`. */
7
+ shimDir: string;
8
+ onPath: boolean;
9
+ /** Where the PATH came from: the persisted registry values, or this process's PATH when those could not be read. */
10
+ source: "registry" | "process";
11
+ }
12
+ export interface ShimPathDeps {
13
+ platform?: NodeJS.Platform;
14
+ /** The running CLI entry script (process.argv[1]). */
15
+ entry?: string;
16
+ exists?: (p: string) => boolean;
17
+ run?: RegRun;
18
+ env?: NodeJS.ProcessEnv;
19
+ }
20
+ /** The npm prefix folder above the package's `node_modules`, when `hq.cmd` is there. */
21
+ export declare function npmShimDir(entry: string | undefined, exists: (p: string) => boolean): string | null;
22
+ /** Expand `%NAME%` references the way Windows does, matching names case-insensitively. */
23
+ export declare function expandWindowsEnv(value: string, env: NodeJS.ProcessEnv): string;
24
+ /**
25
+ * The PATH a new terminal will get: the user Path then the machine Path, as
26
+ * stored in the registry. Falls back to this process's PATH when neither can
27
+ * be read.
28
+ */
29
+ export declare function readPersistedPath(run: RegRun, env: NodeJS.ProcessEnv): {
30
+ value: string;
31
+ source: "registry" | "process";
32
+ };
33
+ export declare function isDirOnPath(dir: string, pathValue: string): boolean;
34
+ /** Null when not on Windows or when hq was not installed as an npm global shim. */
35
+ export declare function checkShimOnPath(deps?: ShimPathDeps): ShimPathCheck | null;
36
+ /** Dummy user variable set and cleared so Windows tells running programs that the environment changed. */
37
+ export declare const PATH_REFRESH_VAR = "HQ_PATH_REFRESH";
38
+ /**
39
+ * A PowerShell command that appends `dir` to the user Path, keeping the value
40
+ * as REG_EXPAND_SZ so existing `%VAR%` entries still expand. Writing the
41
+ * registry alone does not notify Explorer, so terminals it opens would keep the
42
+ * old PATH until the next sign-in. Setting and clearing a dummy user variable
43
+ * through [Environment]::SetEnvironmentVariable broadcasts WM_SETTINGCHANGE.
44
+ */
45
+ export declare function pathFixCommand(dir: string): string;
46
+ /** Lines to print when the shim folder is missing from PATH; empty when it is present. */
47
+ export declare function shimPathWarning(check: ShimPathCheck): string[];
48
+ //# sourceMappingURL=user-path.d.ts.map
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Is the npm global shim folder (where `hq.cmd` lives) on the Windows PATH?
3
+ *
4
+ * npm writes `hq.cmd` directly under its prefix (never `<prefix>\bin`). When
5
+ * that folder is not on the user or machine Path, a new terminal cannot find
6
+ * `hq` even though the install succeeded. HQ detects this and prints the exact
7
+ * fix; it never edits PATH itself.
8
+ */
9
+ import { spawnSync } from "node:child_process";
10
+ import * as fs from "node:fs";
11
+ import * as path from "node:path";
12
+ const USER_ENV_KEY = "HKCU\\Environment";
13
+ const MACHINE_ENV_KEY = "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment";
14
+ /** The npm prefix folder above the package's `node_modules`, when `hq.cmd` is there. */
15
+ export function npmShimDir(entry, exists) {
16
+ if (!entry)
17
+ return null;
18
+ const parts = path.win32.normalize(entry).split("\\");
19
+ const nm = parts.map((p) => p.toLowerCase()).lastIndexOf("node_modules");
20
+ if (nm <= 0)
21
+ return null;
22
+ const dir = parts.slice(0, nm).join("\\");
23
+ return exists(path.win32.join(dir, "hq.cmd")) ? dir : null;
24
+ }
25
+ /** Expand `%NAME%` references the way Windows does, matching names case-insensitively. */
26
+ export function expandWindowsEnv(value, env) {
27
+ return value.replace(/%([^%;]+)%/g, (whole, name) => {
28
+ const key = Object.keys(env).find((k) => k.toLowerCase() === name.toLowerCase());
29
+ const found = key === undefined ? undefined : env[key];
30
+ return found === undefined ? whole : found;
31
+ });
32
+ }
33
+ function regPath(run, key) {
34
+ try {
35
+ const result = run("reg.exe", ["query", key, "/v", "Path"]);
36
+ if (result.status !== 0)
37
+ return null;
38
+ for (const line of result.stdout.split(/\r?\n/)) {
39
+ // An empty value prints no text after the type, which is an empty Path,
40
+ // not an unreadable one.
41
+ const match = /^\s+Path\s+REG_(?:EXPAND_)?SZ(?:\s+(.*))?$/i.exec(line);
42
+ if (match)
43
+ return (match[1] ?? "").trim();
44
+ }
45
+ }
46
+ catch {
47
+ // reg.exe missing or refused: treat as unreadable.
48
+ }
49
+ return null;
50
+ }
51
+ /**
52
+ * The PATH a new terminal will get: the user Path then the machine Path, as
53
+ * stored in the registry. Falls back to this process's PATH when neither can
54
+ * be read.
55
+ */
56
+ export function readPersistedPath(run, env) {
57
+ const values = [regPath(run, USER_ENV_KEY), regPath(run, MACHINE_ENV_KEY)].filter((v) => v !== null);
58
+ if (values.length === 0)
59
+ return { value: env.PATH ?? env.Path ?? "", source: "process" };
60
+ return { value: values.map((v) => expandWindowsEnv(v, env)).join(";"), source: "registry" };
61
+ }
62
+ function normalizeDir(dir) {
63
+ const unquoted = dir.trim().replace(/^"(.*)"$/, "$1").trim();
64
+ if (!unquoted)
65
+ return "";
66
+ return path.win32.normalize(unquoted).replace(/[\\/]+$/, "").toLowerCase();
67
+ }
68
+ export function isDirOnPath(dir, pathValue) {
69
+ const want = normalizeDir(dir);
70
+ return pathValue.split(";").some((entry) => normalizeDir(entry) === want);
71
+ }
72
+ function defaultRun(cmd, args) {
73
+ const result = spawnSync(cmd, args, { encoding: "utf8", windowsHide: true, timeout: 5000 });
74
+ return { status: result.status, stdout: result.stdout ?? "" };
75
+ }
76
+ /** Null when not on Windows or when hq was not installed as an npm global shim. */
77
+ export function checkShimOnPath(deps = {}) {
78
+ if ((deps.platform ?? process.platform) !== "win32")
79
+ return null;
80
+ const shimDir = npmShimDir(deps.entry ?? process.argv[1], deps.exists ?? fs.existsSync);
81
+ if (!shimDir)
82
+ return null;
83
+ const persisted = readPersistedPath(deps.run ?? defaultRun, deps.env ?? process.env);
84
+ return { shimDir, onPath: isDirOnPath(shimDir, persisted.value), source: persisted.source };
85
+ }
86
+ /** Dummy user variable set and cleared so Windows tells running programs that the environment changed. */
87
+ export const PATH_REFRESH_VAR = "HQ_PATH_REFRESH";
88
+ /**
89
+ * A PowerShell command that appends `dir` to the user Path, keeping the value
90
+ * as REG_EXPAND_SZ so existing `%VAR%` entries still expand. Writing the
91
+ * registry alone does not notify Explorer, so terminals it opens would keep the
92
+ * old PATH until the next sign-in. Setting and clearing a dummy user variable
93
+ * through [Environment]::SetEnvironmentVariable broadcasts WM_SETTINGCHANGE.
94
+ */
95
+ export function pathFixCommand(dir) {
96
+ const quoted = dir.replace(/'/g, "''");
97
+ return ("$p = (Get-Item HKCU:\\Environment).GetValue('Path','','DoNotExpandEnvironmentNames'); " +
98
+ "New-ItemProperty -Path HKCU:\\Environment -Name Path -PropertyType ExpandString -Force " +
99
+ `-Value (($p.TrimEnd(';') + ';${quoted}').TrimStart(';')); ` +
100
+ `[Environment]::SetEnvironmentVariable('${PATH_REFRESH_VAR}','1','User'); ` +
101
+ `[Environment]::SetEnvironmentVariable('${PATH_REFRESH_VAR}',$null,'User')`);
102
+ }
103
+ /** Lines to print when the shim folder is missing from PATH; empty when it is present. */
104
+ export function shimPathWarning(check) {
105
+ if (check.onPath)
106
+ return [];
107
+ return [
108
+ `The folder that holds the hq command (${check.shimDir}) is not on your Windows PATH, so a new terminal will not find hq.`,
109
+ "To add it for your user, run this in PowerShell, then open a new terminal:",
110
+ ` ${pathFixCommand(check.shimDir)}`,
111
+ "HQ does not change PATH for you.",
112
+ ];
113
+ }
114
+ //# sourceMappingURL=user-path.js.map
@@ -32,7 +32,7 @@ export type AutoBindDecision = {
32
32
  candidates: AutoBindCandidate[];
33
33
  } | {
34
34
  action: "skip";
35
- reason: "no_git" | "no_company" | "no_match" | "ambiguous";
35
+ reason: "no_git" | "no_company" | "no_match" | "ambiguous" | "foreign_lane";
36
36
  candidates: AutoBindCandidate[];
37
37
  };
38
38
  /** Find the git checkout for cwd and its current branch without spawning git. */
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import * as fs from "node:fs";
12
12
  import * as path from "node:path";
13
+ import { foreignSessionOwnsLane } from "./lane-bind.js";
13
14
  function readText(file) {
14
15
  try {
15
16
  return fs.readFileSync(file, "utf8");
@@ -223,17 +224,35 @@ export function decideAutoBind(input) {
223
224
  identities,
224
225
  branch: git.branch,
225
226
  });
226
- if (prior)
227
+ const ownedByOtherSession = (projectId) => foreignSessionOwnsLane({
228
+ hqRoot: input.hqRoot,
229
+ projectId,
230
+ sessionId: state.sessionId,
231
+ companySlug,
232
+ });
233
+ if (prior) {
234
+ if (ownedByOtherSession(prior)) {
235
+ return { action: "skip", reason: "foreign_lane", candidates, ...extra };
236
+ }
227
237
  return { action: "bind", projectId: prior, source: "history", candidates, ...extra };
238
+ }
228
239
  const branchMatches = candidates.filter((c) => c.matchedBranch);
229
240
  if (branchMatches.length === 1) {
230
- return { action: "bind", projectId: branchMatches[0].projectId, source: "repo_branch", candidates, ...extra };
241
+ const projectId = branchMatches[0].projectId;
242
+ if (ownedByOtherSession(projectId)) {
243
+ return { action: "skip", reason: "foreign_lane", candidates, ...extra };
244
+ }
245
+ return { action: "bind", projectId, source: "repo_branch", candidates, ...extra };
231
246
  }
232
247
  if (branchMatches.length > 1) {
233
248
  return { action: "skip", reason: "ambiguous", candidates: branchMatches, ...extra };
234
249
  }
235
250
  if (candidates.length === 1) {
236
- return { action: "bind", projectId: candidates[0].projectId, source: "repo_only", candidates, ...extra };
251
+ const projectId = candidates[0].projectId;
252
+ if (ownedByOtherSession(projectId)) {
253
+ return { action: "skip", reason: "foreign_lane", candidates, ...extra };
254
+ }
255
+ return { action: "bind", projectId, source: "repo_only", candidates, ...extra };
237
256
  }
238
257
  return {
239
258
  action: "skip",
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Lane ownership for automatic project attachment.
3
+ *
4
+ * A session may attach to a project when it is the senior, the provider
5
+ * session, or the creator of an active lane for that project. It must not
6
+ * attach just because a lane exists, the cwd matches, or a binding is recent.
7
+ * Terminal lanes do not count. A missing lane directory does not block.
8
+ */
9
+ export interface ForeignLaneInput {
10
+ hqRoot?: string;
11
+ projectId?: string;
12
+ sessionId?: string;
13
+ /**
14
+ * Company the bind is for. A lane matches only when its company slug
15
+ * matches too. Missing company, or a lane with no company, does not match.
16
+ */
17
+ companySlug?: string;
18
+ /**
19
+ * Lane records already read during this reconcile, keyed by lane directory.
20
+ * Pass the same map to every check in one reconcile so the lane files are
21
+ * read once.
22
+ */
23
+ laneCache?: LaneRecordCache;
24
+ }
25
+ export type LaneRecordCache = Map<string, Record<string, unknown>[] | null>;
26
+ /**
27
+ * True when an active lane for this company and project is owned by a
28
+ * different session and this session does not own or create one.
29
+ */
30
+ export declare function foreignSessionOwnsLane(input: ForeignLaneInput): boolean;
31
+ //# sourceMappingURL=lane-bind.d.ts.map
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Lane ownership for automatic project attachment.
3
+ *
4
+ * A session may attach to a project when it is the senior, the provider
5
+ * session, or the creator of an active lane for that project. It must not
6
+ * attach just because a lane exists, the cwd matches, or a binding is recent.
7
+ * Terminal lanes do not count. A missing lane directory does not block.
8
+ */
9
+ import * as fs from "node:fs";
10
+ import * as path from "node:path";
11
+ import { TERMINAL_LANE_STATES } from "../lanes/types.js";
12
+ const TERMINAL = new Set(TERMINAL_LANE_STATES);
13
+ function asRecord(value) {
14
+ return value && typeof value === "object" && !Array.isArray(value)
15
+ ? value
16
+ : null;
17
+ }
18
+ function sessionOwnsLane(lane, sessionId) {
19
+ const senior = asRecord(lane.senior);
20
+ if (senior?.kind === "session" && senior.id === sessionId)
21
+ return true;
22
+ if (lane.provider_session_id === sessionId)
23
+ return true;
24
+ if (lane.created_by_session === sessionId)
25
+ return true;
26
+ if (lane.created_by === sessionId)
27
+ return true;
28
+ return false;
29
+ }
30
+ function foreignSessionSenior(lane, sessionId) {
31
+ const senior = asRecord(lane.senior);
32
+ return senior?.kind === "session"
33
+ && typeof senior.id === "string"
34
+ && senior.id.length > 0
35
+ && senior.id !== sessionId;
36
+ }
37
+ /** Personal lanes and lanes with no company do not match a company-scoped bind. */
38
+ function laneCompanyMatches(lane, companySlug) {
39
+ const company = asRecord(lane.company);
40
+ const slug = company && typeof company.slug === "string" ? company.slug.trim() : "";
41
+ if (!slug)
42
+ return false;
43
+ return slug.toLowerCase() === companySlug.toLowerCase();
44
+ }
45
+ /**
46
+ * True when an active lane for this company and project is owned by a
47
+ * different session and this session does not own or create one.
48
+ */
49
+ export function foreignSessionOwnsLane(input) {
50
+ const hqRoot = input.hqRoot?.trim();
51
+ const projectId = input.projectId?.trim();
52
+ const sessionId = input.sessionId?.trim();
53
+ const companySlug = input.companySlug?.trim();
54
+ if (!hqRoot || !projectId || !sessionId || !companySlug)
55
+ return false;
56
+ const dir = path.join(hqRoot, "workspace", "lanes", "lanes");
57
+ const lanes = readLaneRecords(dir, input.laneCache);
58
+ if (!lanes)
59
+ return false;
60
+ let foreign = false;
61
+ let mine = false;
62
+ for (const parsed of lanes) {
63
+ if (parsed.project_id !== projectId)
64
+ continue;
65
+ if (!laneCompanyMatches(parsed, companySlug))
66
+ continue;
67
+ if (typeof parsed.state === "string" && TERMINAL.has(parsed.state))
68
+ continue;
69
+ if (sessionOwnsLane(parsed, sessionId))
70
+ mine = true;
71
+ else if (foreignSessionSenior(parsed, sessionId))
72
+ foreign = true;
73
+ }
74
+ return foreign && !mine;
75
+ }
76
+ function readLaneRecords(dir, cache) {
77
+ if (cache?.has(dir))
78
+ return cache.get(dir) ?? null;
79
+ let records;
80
+ try {
81
+ records = [];
82
+ for (const name of fs.readdirSync(dir)) {
83
+ if (!name.endsWith(".json"))
84
+ continue;
85
+ try {
86
+ const parsed = asRecord(JSON.parse(fs.readFileSync(path.join(dir, name), "utf8")));
87
+ if (parsed)
88
+ records.push(parsed);
89
+ }
90
+ catch {
91
+ // An unreadable lane file does not block.
92
+ }
93
+ }
94
+ }
95
+ catch {
96
+ records = null;
97
+ }
98
+ cache?.set(dir, records);
99
+ return records;
100
+ }
101
+ //# sourceMappingURL=lane-bind.js.map
@@ -451,6 +451,7 @@ export async function submitOrganizeDecision(input) {
451
451
  projectId: receipt.projectId,
452
452
  ...(receipt.taskId ? { taskId: receipt.taskId } : {}),
453
453
  bindingEpisodeId: receipt.bindingEpisodeId,
454
+ projectBindAuthority: "trusted",
454
455
  updatedAt: now,
455
456
  // Clear ask — decision resolved.
456
457
  decision: undefined,
@@ -56,6 +56,9 @@ export function rebindSessionToProject(input) {
56
56
  return null;
57
57
  if (current.projectId === input.projectId)
58
58
  return current;
59
+ // An explicit trusted bind stays until the session rebinds explicitly.
60
+ if (current.projectBindAuthority === "trusted")
61
+ return current;
59
62
  return {
60
63
  ...current,
61
64
  projectId: input.projectId,
@@ -64,6 +67,7 @@ export function rebindSessionToProject(input) {
64
67
  ? "needs_task"
65
68
  : current.contextStatus,
66
69
  projectBindReason: `prd-sync project switch to ${input.projectId}`,
70
+ projectBindAuthority: undefined,
67
71
  updatedAt: nowIso,
68
72
  };
69
73
  });
@@ -117,6 +121,9 @@ export function planPrdSync(input) {
117
121
  return empty("different_project");
118
122
  }
119
123
  const targetProjectId = identity.projectId;
124
+ if (state.projectBindAuthority === "trusted" && state.projectId !== targetProjectId) {
125
+ return empty("different_project");
126
+ }
120
127
  const rebindTo = state.projectId !== targetProjectId ? targetProjectId : undefined;
121
128
  let stories;
122
129
  try {
@@ -2,13 +2,21 @@
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 { type TrustedExplicitContext } from "./contract.js";
19
+ import { type LaneRecordCache } from "./lane-bind.js";
12
20
  import type { SessionStateFile } from "./state.js";
13
21
  export type ProjectResolutionSource = "existing_binding" | "trusted_explicit" | "deterministic_cwd" | "deterministic_registry";
14
22
  export interface ResolvedProjectTask {
@@ -17,16 +25,25 @@ export interface ResolvedProjectTask {
17
25
  source: ProjectResolutionSource;
18
26
  /** True when project+task are both present (trusted or existing bound). */
19
27
  complete: boolean;
28
+ /**
29
+ * Set when the operator or trusted-bind command explicitly chose this
30
+ * project. Later automatic sources must not replace it.
31
+ */
32
+ authority?: "trusted";
20
33
  }
34
+ type TrustedProjectInput = TrustedExplicitContext & {
35
+ companySlug?: string;
36
+ /** Shell and native trusted bind write `project`, not `projectId`. */
37
+ project?: string;
38
+ /** Shell and native trusted bind write `task`, not `taskId`. */
39
+ task?: string;
40
+ /** "trusted" is an explicit bind and beats an existing automatic binding. */
41
+ authority?: string;
42
+ };
21
43
  export interface ProjectResolveInput {
22
44
  sessionId: string;
23
45
  env?: NodeJS.ProcessEnv;
24
- trusted?: TrustedExplicitContext & {
25
- companySlug?: string;
26
- projectId?: string;
27
- taskId?: string;
28
- storyId?: string;
29
- };
46
+ trusted?: TrustedProjectInput;
30
47
  cwd?: string;
31
48
  hqRoot?: string;
32
49
  existingState?: SessionStateFile | null;
@@ -35,6 +52,14 @@ export interface ProjectResolveInput {
35
52
  metaTaskId?: string | null;
36
53
  /** Injectable registry project id (tests). */
37
54
  registryProjectId?: string | null;
55
+ /**
56
+ * Company reconcile already resolved. A trusted project is pinned only when
57
+ * that company's project directory exists. Missing company or root refuses
58
+ * the pin.
59
+ */
60
+ resolvedCompanySlug?: string;
61
+ /** Lane records shared across the lane checks of one reconcile. */
62
+ laneCache?: LaneRecordCache;
38
63
  }
39
64
  /**
40
65
  * Read project / task from workspace/sessions/<sid>/meta.yaml (best-effort).
@@ -75,4 +100,5 @@ export declare function shouldAskAfter(input: {
75
100
  hasTurn?: boolean;
76
101
  now?: Date;
77
102
  }): boolean;
103
+ export {};
78
104
  //# sourceMappingURL=project.d.ts.map