@indigoai-us/hq-cli 5.125.0 → 5.127.0

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 (108) hide show
  1. package/CHANGELOG.md +329 -0
  2. package/assets/bot-workers/setup/skills/joining-a-team.md +125 -0
  3. package/assets/bot-workers/setup/skills/your-business.md +270 -0
  4. package/assets/bot-workers/setup/worker.yaml +189 -58
  5. package/assets/lanes-hooks/hq-lanes-drain.json +15 -0
  6. package/assets/lanes-hooks/hq-lanes-drain.mjs +254 -0
  7. package/assets/lanes-hooks/hq-lanes-drain.sh +21 -0
  8. package/assets/lanes-reconcile/hq-lanes-reconcile.service +11 -0
  9. package/assets/lanes-reconcile/hq-lanes-reconcile.timer +12 -0
  10. package/dist/command-catalog.generated.d.ts +643 -6
  11. package/dist/command-catalog.generated.js +836 -6
  12. package/dist/command-registration-plan.d.ts +7 -0
  13. package/dist/command-registration-plan.js +11 -1
  14. package/dist/commands/agents.d.ts +12 -0
  15. package/dist/commands/agents.js +46 -2
  16. package/dist/commands/bot.d.ts +10 -0
  17. package/dist/commands/bot.js +6 -0
  18. package/dist/commands/lanes.d.ts +114 -0
  19. package/dist/commands/lanes.js +2969 -0
  20. package/dist/commands/mesh.js +287 -24
  21. package/dist/commands/onboard-create-identity.d.ts +26 -0
  22. package/dist/commands/onboard-create-identity.js +23 -0
  23. package/dist/commands/onboard.js +13 -4
  24. package/dist/lib/bot/progress.d.ts +6 -0
  25. package/dist/lib/bot/progress.js +16 -0
  26. package/dist/lib/bot/runtime/grok.d.ts +30 -7
  27. package/dist/lib/bot/runtime/grok.js +86 -8
  28. package/dist/lib/bot/runtime/index.js +15 -8
  29. package/dist/lib/bot/runtime/messages-stream.d.ts +12 -3
  30. package/dist/lib/bot/runtime/messages-stream.js +49 -3
  31. package/dist/lib/lanes/accounts.d.ts +103 -0
  32. package/dist/lib/lanes/accounts.js +318 -0
  33. package/dist/lib/lanes/compaction.d.ts +96 -0
  34. package/dist/lib/lanes/compaction.js +197 -0
  35. package/dist/lib/lanes/config.d.ts +71 -0
  36. package/dist/lib/lanes/config.js +408 -0
  37. package/dist/lib/lanes/delivery.d.ts +151 -0
  38. package/dist/lib/lanes/delivery.js +354 -0
  39. package/dist/lib/lanes/dropbox.d.ts +96 -0
  40. package/dist/lib/lanes/dropbox.js +408 -0
  41. package/dist/lib/lanes/effort.d.ts +50 -0
  42. package/dist/lib/lanes/effort.js +109 -0
  43. package/dist/lib/lanes/errors.d.ts +39 -0
  44. package/dist/lib/lanes/errors.js +72 -0
  45. package/dist/lib/lanes/hooks-install.d.ts +81 -0
  46. package/dist/lib/lanes/hooks-install.js +326 -0
  47. package/dist/lib/lanes/hq-root.d.ts +19 -0
  48. package/dist/lib/lanes/hq-root.js +28 -0
  49. package/dist/lib/lanes/index.d.ts +49 -0
  50. package/dist/lib/lanes/index.js +27 -0
  51. package/dist/lib/lanes/liveness.d.ts +95 -0
  52. package/dist/lib/lanes/liveness.js +332 -0
  53. package/dist/lib/lanes/machine-identity.d.ts +34 -0
  54. package/dist/lib/lanes/machine-identity.js +92 -0
  55. package/dist/lib/lanes/mesh-events.d.ts +109 -0
  56. package/dist/lib/lanes/mesh-events.js +275 -0
  57. package/dist/lib/lanes/outpost-dispatch.d.ts +65 -0
  58. package/dist/lib/lanes/outpost-dispatch.js +90 -0
  59. package/dist/lib/lanes/questions.d.ts +75 -0
  60. package/dist/lib/lanes/questions.js +243 -0
  61. package/dist/lib/lanes/quota.d.ts +95 -0
  62. package/dist/lib/lanes/quota.js +320 -0
  63. package/dist/lib/lanes/reconcile-install.d.ts +69 -0
  64. package/dist/lib/lanes/reconcile-install.js +221 -0
  65. package/dist/lib/lanes/reconcile.d.ts +102 -0
  66. package/dist/lib/lanes/reconcile.js +985 -0
  67. package/dist/lib/lanes/registry.d.ts +116 -0
  68. package/dist/lib/lanes/registry.js +521 -0
  69. package/dist/lib/lanes/spawn.d.ts +274 -0
  70. package/dist/lib/lanes/spawn.js +1363 -0
  71. package/dist/lib/lanes/store.d.ts +68 -0
  72. package/dist/lib/lanes/store.js +745 -0
  73. package/dist/lib/lanes/throttle.d.ts +89 -0
  74. package/dist/lib/lanes/throttle.js +297 -0
  75. package/dist/lib/lanes/types.d.ts +535 -0
  76. package/dist/lib/lanes/types.js +350 -0
  77. package/dist/lib/lanes/watcher-main.d.ts +9 -0
  78. package/dist/lib/lanes/watcher-main.js +22 -0
  79. package/dist/lib/lanes/watcher.d.ts +216 -0
  80. package/dist/lib/lanes/watcher.js +859 -0
  81. package/dist/lib/mesh/api.d.ts +71 -7
  82. package/dist/lib/mesh/api.js +167 -28
  83. package/dist/lib/mesh/live/backfill-held.d.ts +18 -7
  84. package/dist/lib/mesh/live/backfill-held.js +66 -21
  85. package/dist/lib/mesh/live/daemon/config.d.ts +27 -0
  86. package/dist/lib/mesh/live/daemon/config.js +75 -0
  87. package/dist/lib/mesh/live/daemon/index.d.ts +4 -3
  88. package/dist/lib/mesh/live/daemon/index.js +3 -3
  89. package/dist/lib/mesh/live/daemon/install.d.ts +143 -1
  90. package/dist/lib/mesh/live/daemon/install.js +444 -2
  91. package/dist/lib/mesh/live/daemon/paths.d.ts +2 -0
  92. package/dist/lib/mesh/live/daemon/paths.js +2 -0
  93. package/dist/lib/mesh/live/daemon/run.d.ts +2 -0
  94. package/dist/lib/mesh/live/daemon/run.js +111 -54
  95. package/dist/lib/mesh/live/daemon/state.d.ts +2 -0
  96. package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +3 -0
  97. package/dist/lib/mesh/live/daemon/transcript-watch.js +31 -8
  98. package/dist/lib/scan-packages/index.d.ts +2 -0
  99. package/dist/lib/scan-packages/index.js +18 -13
  100. package/dist/lib/work-context/config.d.ts +20 -0
  101. package/dist/lib/work-context/config.js +26 -5
  102. package/dist/lib/work-context/reconcile.js +28 -7
  103. package/dist/lib/work-context/state.d.ts +12 -0
  104. package/dist/lib/work-context/state.js +131 -3
  105. package/dist/lib/work-context/windows-acl.d.ts +123 -0
  106. package/dist/lib/work-context/windows-acl.js +425 -0
  107. package/dist/utils/pack-contributions.js +5 -4
  108. package/package.json +3 -1
@@ -3,9 +3,10 @@
3
3
  * Written only by hq mesh context. Owner 0600. Metadata-only — never prompts/tokens.
4
4
  */
5
5
  import * as fs from "node:fs";
6
+ import * as path from "node:path";
6
7
  import { WORK_CONTEXT_CONTRACT_VERSION } from "./contract.js";
7
- import { atomicWriteJson, ensureOwnerDir } from "./atomic.js";
8
- import { NotTrackingError } from "./errors.js";
8
+ import { atomicWriteJson, ensureOwnerDir, resolveRealTarget } from "./atomic.js";
9
+ import { NotTrackingError, UnsafeConfigPathError } from "./errors.js";
9
10
  import { isSafeWorkContextSegment, workContextSessionPath, workContextSessionsDir, } from "./paths.js";
10
11
  /** Allowlisted keys that may appear in the durable state projection. */
11
12
  export const SESSION_STATE_ALLOWLIST = new Set([
@@ -30,6 +31,113 @@ export const SESSION_STATE_ALLOWLIST = new Set([
30
31
  "companyConfidence",
31
32
  "updatedAt",
32
33
  ]);
34
+ const SESSION_STATE_LOCK_TIMEOUT_MS = 5_000;
35
+ const SESSION_STATE_LOCK_RETRY_MS = 10;
36
+ function waitForSessionStateLock(ms) {
37
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
38
+ }
39
+ function asPersistenceFailure(err) {
40
+ if (err instanceof NotTrackingError || err instanceof UnsafeConfigPathError)
41
+ throw err;
42
+ const message = err instanceof Error ? err.message : String(err);
43
+ throw new NotTrackingError(`Failed to persist session state: ${message}`, "NOT_TRACKING");
44
+ }
45
+ function reclaimStaleSessionStateLock(lockPath) {
46
+ const claim = `${lockPath}.reclaim.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
47
+ try {
48
+ fs.renameSync(lockPath, claim);
49
+ }
50
+ catch (err) {
51
+ if (err.code === "ENOENT")
52
+ return false;
53
+ throw err;
54
+ }
55
+ try {
56
+ return true;
57
+ }
58
+ finally {
59
+ try {
60
+ fs.unlinkSync(claim);
61
+ }
62
+ catch { /* private claim cannot block writers */ }
63
+ }
64
+ }
65
+ /**
66
+ * Serialize a session-state read/modify/write with every state writer. Atomic
67
+ * rename protects readers from torn files, but only this shared lock protects
68
+ * a writer that has to verify the current binding before changing it.
69
+ */
70
+ export function withSessionStateLock(sessionId, root, fn) {
71
+ if (!isSafeWorkContextSegment(sessionId)) {
72
+ throw new NotTrackingError(`Unsafe session id for state file: ${sessionId}`, "NOT_TRACKING");
73
+ }
74
+ try {
75
+ ensureOwnerDir(workContextSessionsDir(root));
76
+ }
77
+ catch (err) {
78
+ asPersistenceFailure(err);
79
+ }
80
+ const statePath = resolveRealTarget(workContextSessionPath(sessionId, root));
81
+ const lockPath = `${statePath}.lock`;
82
+ const token = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
83
+ const deadline = Date.now() + SESSION_STATE_LOCK_TIMEOUT_MS;
84
+ let fd;
85
+ while (fd === undefined) {
86
+ try {
87
+ fd = fs.openSync(lockPath, "wx", 0o600);
88
+ fs.writeSync(fd, JSON.stringify({ pid: process.pid, token }));
89
+ }
90
+ catch (err) {
91
+ if (err.code !== "EEXIST")
92
+ asPersistenceFailure(err);
93
+ let ownerPid = 0;
94
+ try {
95
+ const raw = JSON.parse(fs.readFileSync(lockPath, "utf8"));
96
+ ownerPid = typeof raw.pid === "number" && Number.isInteger(raw.pid) ? raw.pid : 0;
97
+ }
98
+ catch {
99
+ // A malformed lock cannot safely protect the state.
100
+ }
101
+ let ownerAlive = false;
102
+ if (ownerPid > 0) {
103
+ try {
104
+ process.kill(ownerPid, 0);
105
+ ownerAlive = true;
106
+ }
107
+ catch {
108
+ // Dead owners do not retain the lock.
109
+ }
110
+ }
111
+ if (!ownerAlive) {
112
+ try {
113
+ reclaimStaleSessionStateLock(lockPath);
114
+ }
115
+ catch (err) {
116
+ asPersistenceFailure(err);
117
+ }
118
+ continue;
119
+ }
120
+ if (Date.now() >= deadline) {
121
+ throw new NotTrackingError(`Timed out waiting for session state lock: ${path.basename(statePath)}`, "NOT_TRACKING");
122
+ }
123
+ waitForSessionStateLock(SESSION_STATE_LOCK_RETRY_MS);
124
+ }
125
+ }
126
+ try {
127
+ return fn();
128
+ }
129
+ finally {
130
+ fs.closeSync(fd);
131
+ try {
132
+ const current = JSON.parse(fs.readFileSync(lockPath, "utf8"));
133
+ if (current.token === token)
134
+ fs.unlinkSync(lockPath);
135
+ }
136
+ catch {
137
+ // A stale-lock reclaimer or replacement owner has already retired it.
138
+ }
139
+ }
140
+ }
33
141
  export function readSessionState(sessionId, root) {
34
142
  if (!isSafeWorkContextSegment(sessionId))
35
143
  return null;
@@ -50,6 +158,9 @@ export function writeSessionState(state, root) {
50
158
  if (!isSafeWorkContextSegment(state.sessionId)) {
51
159
  throw new NotTrackingError(`Unsafe session id for state file: ${state.sessionId}`, "NOT_TRACKING");
52
160
  }
161
+ return withSessionStateLock(state.sessionId, root, () => writeSessionStateUnlocked(state, root));
162
+ }
163
+ function writeSessionStateUnlocked(state, root) {
53
164
  const projected = projectSessionState(state);
54
165
  try {
55
166
  ensureOwnerDir(workContextSessionsDir(root));
@@ -61,6 +172,20 @@ export function writeSessionState(state, root) {
61
172
  throw new NotTrackingError(`Failed to persist session state: ${err instanceof Error ? err.message : String(err)}`, "NOT_TRACKING");
62
173
  }
63
174
  }
175
+ /**
176
+ * Commit a state change only after inspecting the latest value under the same
177
+ * lock used by ordinary session writers. Returning null declines the write.
178
+ */
179
+ export function updateSessionState(sessionId, root, update) {
180
+ return withSessionStateLock(sessionId, root, () => {
181
+ const current = readSessionState(sessionId, root);
182
+ const next = update(current);
183
+ if (!next)
184
+ return null;
185
+ writeSessionStateUnlocked(next, root);
186
+ return next;
187
+ });
188
+ }
64
189
  /** Strip any non-allowlisted fields before durable write. */
65
190
  export function projectSessionState(state) {
66
191
  const out = {
@@ -118,7 +243,10 @@ export function mergeLocalSessionFields(next, prior, options = {}) {
118
243
  // projected locally; losing it causes held events and repeat registrations.
119
244
  // A caller that carries either company field is intentionally changing the
120
245
  // binding and remains authoritative.
121
- if (!out.companyUid && !out.companySlug && (prior.companyUid || prior.companySlug)) {
246
+ if (options.preserveCompanyBinding !== false &&
247
+ !out.companyUid &&
248
+ !out.companySlug &&
249
+ (prior.companyUid || prior.companySlug)) {
122
250
  out.companyUid = prior.companyUid;
123
251
  out.companySlug = prior.companySlug;
124
252
  out.contextStatus = prior.contextStatus;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Windows DACL check for the work-context config (US-018).
3
+ *
4
+ * NTFS has no POSIX mode bits: Node reports `mode` 0666 for every regular
5
+ * file on win32, so the POSIX `mode & 0o022` test in config.ts can never pass
6
+ * there. Instead of skipping the check on Windows, this module reads the real
7
+ * DACL and refuses the config when any principal other than the current user,
8
+ * SYSTEM, Administrators, or the owner is granted write/modify/full access.
9
+ *
10
+ * How the DACL is read (no native dependency):
11
+ * `icacls <path> /save <tmpfile>` writes the DACL as an SDDL string
12
+ * (`D:AI(A;ID;FA;;;SY)(A;ID;FA;;;BA)(A;ID;FA;;;S-1-5-21-...)`). SDDL carries
13
+ * SIDs or well-known SID abbreviations, never display names, so the check is
14
+ * independent of the OS language (VORDEFINIERT\Administratoren is still BA /
15
+ * S-1-5-32-544). The current user's SID comes from `whoami /user`. Display
16
+ * names are only resolved (via PowerShell) for the error message.
17
+ *
18
+ * Trust is decided by SID: S-1-5-18 (SYSTEM), S-1-5-32-544 (Administrators),
19
+ * S-1-3-0 (CREATOR OWNER), S-1-3-4 (OWNER RIGHTS), the current user's SID and
20
+ * the owner's SID when known.
21
+ *
22
+ * Reduced-guarantee path (explicit, documented): the check does NOT throw and
23
+ * instead emits one stderr diagnostic that the config is unprotected on this
24
+ * platform when, and only when, the DACL cannot be evaluated:
25
+ * - icacls is missing, fails, or its output has no SDDL line (unreadable);
26
+ * - an ACE grants write to a principal whose SID we cannot resolve (an SDDL
27
+ * abbreviation this module does not know), or carries a rights token we
28
+ * cannot decode, or the current user's SID is unknown and the grant is to
29
+ * a non-well-known SID (unresolvable).
30
+ * A DACL that parses and grants write to a resolved SID outside the trusted
31
+ * set always throws UnsafeConfigPathError. Nothing else falls back.
32
+ */
33
+ export interface SddlAce {
34
+ /** ACE type token, e.g. `A` (allow) or `D` (deny). */
35
+ type: string;
36
+ /** ACE flag string, e.g. `OICIID`. */
37
+ flags: string;
38
+ /** Rights field as written, e.g. `FA`, `0x1200a9`, `CCDCLCSWRPWPRC`. */
39
+ rights: string;
40
+ /** Account field as written: full SID or two-letter abbreviation. */
41
+ account: string;
42
+ }
43
+ export type AclReadResult = {
44
+ readable: true;
45
+ sddl: string;
46
+ } | {
47
+ readable: false;
48
+ reason: string;
49
+ };
50
+ export interface WindowsAclCheckOptions {
51
+ /** Current user SID (defaults to `whoami /user`). `null` = unknown. */
52
+ currentUserSid?: string | null;
53
+ /** Owner SID when known (icacls /save does not include the owner). */
54
+ ownerSid?: string | null;
55
+ /** DACL reader; injectable for tests. Defaults to `icacls <path> /save`. */
56
+ readAcl?: (targetPath: string) => AclReadResult;
57
+ /** SID → display name for error messages; injectable for tests. */
58
+ resolveSidName?: (sid: string) => string | null;
59
+ /** Diagnostic sink for the reduced-guarantee path (defaults to stderr). */
60
+ diagnostic?: (line: string) => void;
61
+ }
62
+ export type AclVerdict = {
63
+ kind: "safe";
64
+ } | {
65
+ kind: "unsafe";
66
+ sid: string;
67
+ rights: string;
68
+ } | {
69
+ kind: "unresolvable";
70
+ reason: string;
71
+ };
72
+ export declare const SID_SYSTEM = "S-1-5-18";
73
+ export declare const SID_ADMINISTRATORS = "S-1-5-32-544";
74
+ export declare const SID_CREATOR_OWNER = "S-1-3-0";
75
+ export declare const SID_OWNER_RIGHTS = "S-1-3-4";
76
+ /** Resolve an SDDL account field to a SID, or null when unknown. */
77
+ export declare function sddlAccountToSid(account: string): string | null;
78
+ /** Any of these bits lets the holder change the file or replace it. */
79
+ export declare const WRITE_MASK: number;
80
+ /** Decode an SDDL rights field to an access mask, or null when undecodable. */
81
+ export declare function sddlRightsToMask(rights: string): number | null;
82
+ /**
83
+ * Parse the DACL portion of an SDDL string into ACEs. Accepts a full security
84
+ * descriptor string (`O:...G:...D:...S:...`) or just the `D:` section.
85
+ * Returns null when no DACL section is present or an ACE is malformed.
86
+ */
87
+ export declare function parseSddlDacl(sddl: string): SddlAce[] | null;
88
+ /**
89
+ * Parse the file `icacls <path> /save <file>` writes: UTF-16LE (optional BOM),
90
+ * CRLF, alternating `<name>` / `<sddl>` lines. Returns the SDDL of the first
91
+ * entry (the target itself), or an unreadable result.
92
+ */
93
+ export declare function parseIcaclsSaveOutput(raw: string | Buffer): AclReadResult;
94
+ /**
95
+ * Absolute path of a System32 tool. PATH lookup is not safe here: Git Bash and
96
+ * MSYS environments put a coreutils `whoami` ahead of Windows' `whoami.exe`,
97
+ * which does not understand `/user` and would make the current user SID look
98
+ * unknown on every run.
99
+ */
100
+ export declare function system32Tool(exe: string): string;
101
+ export declare function readAclWithIcaclsSave(targetPath: string): AclReadResult;
102
+ /** Current user's SID via `whoami /user /fo csv /nh`; null when unavailable. */
103
+ export declare function readCurrentUserSid(): string | null;
104
+ /** Best-effort SID → account name (only used to build error messages). */
105
+ export declare function resolveSidNameWithPowerShell(sid: string): string | null;
106
+ /**
107
+ * Evaluate parsed ACEs against the trusted SID set.
108
+ * - `unsafe`: an allow ACE grants a write bit to a resolved, untrusted SID.
109
+ * - `unresolvable`: a write-granting ACE whose SID or rights cannot be decoded,
110
+ * or whose SID is a per-machine/domain SID while the current user is unknown.
111
+ * - `safe`: everything else (deny ACEs and read-only grants are ignored).
112
+ */
113
+ export declare function evaluateDacl(aces: SddlAce[], opts: Pick<WindowsAclCheckOptions, "currentUserSid" | "ownerSid">): AclVerdict;
114
+ /**
115
+ * Assert the Windows DACL of `targetPath` grants write only to trusted
116
+ * principals. Throws UnsafeConfigPathError naming the offending principal.
117
+ * Unreadable or unresolvable DACL → reduced-guarantee diagnostic (once per
118
+ * path), no throw. See the module header for the exact conditions.
119
+ */
120
+ export declare function assertWindowsOwnerOnlyAcl(targetPath: string, kind: "file" | "directory", opts?: WindowsAclCheckOptions): void;
121
+ /** Test hook: forget which paths already emitted the reduced-guarantee line. */
122
+ export declare function resetWindowsAclDiagnostics(): void;
123
+ //# sourceMappingURL=windows-acl.d.ts.map