@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
@@ -6,11 +6,12 @@
6
6
  * Distinct from `hq doctor` (hook guardrails).
7
7
  */
8
8
  import * as os from "node:os";
9
+ import { readFileSync } from "node:fs";
9
10
  import { findHqRoot } from "../utils/manifest.js";
10
11
  import chalk from "chalk";
11
12
  import { loadCachedTokens, isMachineIdentity } from "../utils/cognito-session.js";
12
13
  import * as readline from "node:readline/promises";
13
- import { STORY_STATUSES, appendThreadEvent, callerLabelFromToken, ensureProjectThread, eventPayload, listActiveMembershipCompanies, listActiveThreads, patchStoryStatus, resolveActiveMembershipCompany, resolveMeshPrincipalUid, warmMeshConversationCache, } from "../lib/mesh/api.js";
14
+ import { STORY_STATUSES, appendThreadEvent, ensureProjectThread, eventPayload, getProjectView, listActiveMembershipCompanies, listActiveThreads, patchStory, putProjectView, registerMeshProject, resolveActiveMembershipCompany, resolveMeshPrincipalUid, warmMeshConversationCache, } from "../lib/mesh/api.js";
14
15
  import { createCandidatesFetcher, createMigratePoster, createOrganizePoster, createWorkSessionDeliverer, fetchCompanyLive, formatCompanyLiveTable, openMeshTransport, probeMigrationCapabilityForMemberships, requireToken, } from "../lib/mesh/client.js";
15
16
  import { clearDefaultCompany, isDefaultCompanyDisabled, readDeviceConfig, recordMigrationCapabilitySnapshot, setRepairHeldWithDefault, setDefaultCompany, } from "../lib/work-context/config.js";
16
17
  import { ensureDefaultCompany } from "../lib/work-context/default-company.js";
@@ -29,7 +30,7 @@ import { generateUlid } from "../lib/mesh/live/ulid.js";
29
30
  import { readLegacyBacklog } from "../lib/mesh/live/emit-replay.js";
30
31
  import { resolveMeshEmitMode, writeMeshConfigMode, } from "../lib/mesh/live/daemon/mode.js";
31
32
  import { workMeshRoot } from "../lib/mesh/live/paths.js";
32
- import { buildInstallPaths, collectDaemonDoctor, daemonServiceStatus, detectPlatform, formatDaemonDoctor, installDaemonService, readDaemonState, runMeshDaemon, uninstallDaemonService, daemonDir, } from "../lib/mesh/live/daemon/index.js";
33
+ import { buildInstallPaths, collectDaemonDoctor, daemonServiceStatus, findAncestorHqRoot, resolveInstallHqRoot, detectPlatform, formatDaemonDoctor, installDaemonService, readDaemonState, runMeshDaemon, uninstallDaemonService, daemonDir, } from "../lib/mesh/live/daemon/index.js";
33
34
  import { workContextRoot } from "../lib/work-context/paths.js";
34
35
  import { formatMigrateConfirmation, submitSessionMigration, } from "../lib/work-context/migrate.js";
35
36
  import { formatOrganizeList, prepareOrganizeDecision, settleOrganizeAskWithoutBind, submitOrganizeDecision, } from "../lib/work-context/organize.js";
@@ -88,12 +89,20 @@ async function runCheck(opts) {
88
89
  async function runEvent(verb, opts) {
89
90
  const { token, company } = await withCompany(opts);
90
91
  const projectId = requireProject(opts);
92
+ const eventKind = verb === "start" ? "claim" : verb;
93
+ let claimant = "";
94
+ if (eventKind === "claim") {
95
+ const resolvedClaimant = await resolveMeshPrincipalUid(token, loadCachedTokens());
96
+ if (!resolvedClaimant) {
97
+ fail("Could not resolve the authenticated claimant. Sign in again and retry `hq mesh start`.");
98
+ }
99
+ claimant = resolvedClaimant;
100
+ }
91
101
  const ensured = await ensureProjectThread(token, company, projectId, {
92
102
  threadId: opts.threadId,
93
103
  summary: opts.summary,
94
104
  });
95
- const eventKind = verb === "start" ? "claim" : verb;
96
- const event = await appendThreadEvent(token, company.companyUid, ensured.threadId, eventKind, eventPayload(eventKind, opts, callerLabelFromToken(token)));
105
+ const event = await appendThreadEvent(token, company.companyUid, ensured.threadId, eventKind, eventPayload(eventKind, opts, claimant));
97
106
  if (opts.json) {
98
107
  console.log(JSON.stringify({
99
108
  ok: true,
@@ -112,17 +121,215 @@ async function runEvent(verb, opts) {
112
121
  async function runStory(opts) {
113
122
  const storyId = opts.story?.trim();
114
123
  const status = opts.status?.trim();
115
- if (!storyId || !status || !STORY_STATUSES.has(status)) {
116
- fail("story requires --story <id> and --status queued|in_progress|review|done");
124
+ const assignee = opts.assignee?.trim();
125
+ if (!storyId) {
126
+ fail("story requires --story <id>");
127
+ }
128
+ if (!status && !assignee) {
129
+ fail("story requires --status queued|in_progress|review|done or --assignee <uid|me|none>");
130
+ }
131
+ if (status && !STORY_STATUSES.has(status)) {
132
+ fail("story --status must be queued|in_progress|review|done");
117
133
  }
118
134
  const { token, company } = await withCompany(opts);
119
135
  const projectId = requireProject(opts);
120
- const patched = await patchStoryStatus(token, company.companyUid, projectId, storyId, status);
136
+ let assigneeUid;
137
+ let clearAssignee = false;
138
+ if (assignee === "none") {
139
+ clearAssignee = true;
140
+ }
141
+ else if (assignee === "me") {
142
+ const resolved = await resolveMeshPrincipalUid(token, loadCachedTokens());
143
+ if (!resolved || resolved === "session") {
144
+ fail("`--assignee me` needs the signed-in person or agent. The synthetic session identity cannot be assigned. Sign in again and retry `hq mesh story`.");
145
+ }
146
+ assigneeUid = resolved;
147
+ }
148
+ else if (assignee) {
149
+ if (!/^(prs|agt)_/.test(assignee)) {
150
+ fail("`--assignee` must be a person or agent uid, `me`, or `none`.");
151
+ }
152
+ assigneeUid = assignee;
153
+ }
154
+ const patched = await patchStory(token, company.companyUid, projectId, storyId, {
155
+ ...(status ? { status } : {}),
156
+ ...(assigneeUid ? { assigneeUid } : {}),
157
+ ...(clearAssignee ? { clearAssignee: true } : {}),
158
+ });
121
159
  if (opts.json) {
122
160
  console.log(JSON.stringify({ ok: true, action: "story", company, ...patched, storyId }, null, 2));
123
161
  return;
124
162
  }
125
- console.log(`Work mesh: story ${storyId} → ${patched.status} (v${patched.version ?? "?"})`);
163
+ const assignment = assignee === "none"
164
+ ? "assignee cleared"
165
+ : assignee
166
+ ? `assignee ${patched.assigneeUid ?? assigneeUid}`
167
+ : "";
168
+ const statusPart = status ? `${storyId} → ${patched.status}` : storyId;
169
+ console.log(`Work mesh: story ${statusPart}${assignment ? ` ${assignment}` : ""} (v${patched.version ?? "?"})`);
170
+ }
171
+ function record(value) {
172
+ return value && typeof value === "object" && !Array.isArray(value)
173
+ ? value
174
+ : null;
175
+ }
176
+ function parseProjectRepoSpec(value, source) {
177
+ const raw = value.trim();
178
+ const separator = raw.lastIndexOf("@");
179
+ const path = (separator > 0 ? raw.slice(0, separator) : raw).trim();
180
+ const branch = (separator > 0 ? raw.slice(separator + 1) : "").trim();
181
+ if (!path)
182
+ fail(`${source} requires a non-empty repository path.`);
183
+ if (separator > 0 && !branch)
184
+ fail(`${source} has an empty branch after @.`);
185
+ return { path, branch };
186
+ }
187
+ function parseProjectRepos(value, source) {
188
+ if (!Array.isArray(value))
189
+ fail(`${source} must be a JSON repository array.`);
190
+ return value.map((raw, index) => {
191
+ if (typeof raw === "string")
192
+ return parseProjectRepoSpec(raw, `${source}[${index}]`);
193
+ const repo = record(raw);
194
+ const path = typeof repo?.path === "string"
195
+ ? repo.path.trim()
196
+ : typeof repo?.repoPath === "string"
197
+ ? repo.repoPath.trim()
198
+ : typeof repo?.repo === "string"
199
+ ? repo.repo.trim()
200
+ : "";
201
+ const branchValue = repo?.branch ?? repo?.branchName;
202
+ const branch = branchValue === undefined ? "" : typeof branchValue === "string" ? branchValue.trim() : null;
203
+ if (!repo || !path)
204
+ fail(`${source}[${index}].path requires a string.`);
205
+ if (branch === null)
206
+ fail(`${source}[${index}].branch requires a string.`);
207
+ return { path, branch };
208
+ });
209
+ }
210
+ function parseProjectReposJson(raw, source) {
211
+ try {
212
+ return parseProjectRepos(JSON.parse(raw), source);
213
+ }
214
+ catch (err) {
215
+ if (err instanceof SyntaxError)
216
+ fail(`${source} must contain valid JSON.`);
217
+ throw err;
218
+ }
219
+ }
220
+ function projectStoriesFromJson(value, source) {
221
+ const plan = Array.isArray(value) ? null : record(value);
222
+ const rawStories = Array.isArray(value)
223
+ ? value
224
+ : Array.isArray(plan?.userStories)
225
+ ? plan.userStories
226
+ : Array.isArray(plan?.stories)
227
+ ? plan.stories
228
+ : null;
229
+ if (!rawStories) {
230
+ fail(`${source} must be a JSON story array or an object with userStories.`);
231
+ }
232
+ const stories = rawStories.map((raw, index) => {
233
+ const story = record(raw);
234
+ const id = typeof story?.id === "string" ? story.id.trim() : "";
235
+ const title = typeof story?.title === "string" ? story.title.trim() : "";
236
+ if (!story || !id || !title) {
237
+ fail(`${source} userStories[${index}] requires string id and title.`);
238
+ }
239
+ const status = typeof story.status === "string" ? story.status.trim() : undefined;
240
+ if (status && !STORY_STATUSES.has(status)) {
241
+ fail(`${source} userStories[${index}].status must be queued|in_progress|review|done.`);
242
+ }
243
+ const resolvedStatus = status || (story.passes === true ? "done" : undefined);
244
+ return {
245
+ id,
246
+ title,
247
+ ...(typeof story.description === "string" ? { description: story.description } : {}),
248
+ ...(Array.isArray(story.acceptanceCriteria)
249
+ ? {
250
+ acceptanceCriteria: story.acceptanceCriteria.filter((item) => typeof item === "string"),
251
+ }
252
+ : {}),
253
+ ...(resolvedStatus ? { status: resolvedStatus } : {}),
254
+ // hq-pro makes passes mirror the Board status. A completed PRD story
255
+ // without an explicit status must therefore become done.
256
+ passes: resolvedStatus === "done",
257
+ ...(typeof story.priority === "number" && Number.isFinite(story.priority)
258
+ ? { priority: story.priority }
259
+ : {}),
260
+ };
261
+ });
262
+ const metadata = record(plan?.metadata);
263
+ const repos = metadata && Object.hasOwn(metadata, "repos")
264
+ ? parseProjectRepos(metadata.repos, `${source} metadata.repos`)
265
+ : undefined;
266
+ return {
267
+ ...(typeof plan?.name === "string" ? { name: plan.name } : {}),
268
+ ...(typeof plan?.description === "string" ? { description: plan.description } : {}),
269
+ stories,
270
+ ...(repos ? { repos } : {}),
271
+ };
272
+ }
273
+ function parseProjectPlan(raw, source) {
274
+ try {
275
+ return projectStoriesFromJson(JSON.parse(raw), source);
276
+ }
277
+ catch (err) {
278
+ if (err instanceof SyntaxError)
279
+ fail(`${source} must contain valid JSON.`);
280
+ throw err;
281
+ }
282
+ }
283
+ async function runProjectSet(projectId, opts) {
284
+ if (opts.storiesFile && opts.storiesJson) {
285
+ fail("Pass only one of --stories-file or --stories-json.");
286
+ }
287
+ if (opts.reposJson && opts.repo?.length) {
288
+ fail("Pass only one of --repos-json or --repo.");
289
+ }
290
+ const source = opts.storiesFile
291
+ ? parseProjectPlan(readFileSync(opts.storiesFile, "utf8"), opts.storiesFile)
292
+ : opts.storiesJson
293
+ ? parseProjectPlan(opts.storiesJson, "--stories-json")
294
+ : undefined;
295
+ const explicitRepos = opts.reposJson
296
+ ? parseProjectReposJson(opts.reposJson, "--repos-json")
297
+ : opts.repo?.length
298
+ ? opts.repo.map((repo, index) => parseProjectRepoSpec(repo, `--repo[${index}]`))
299
+ : undefined;
300
+ const { token, company } = await withCompany(opts);
301
+ const current = await getProjectView(token, company.companyUid, projectId);
302
+ const principalUid = await resolveMeshPrincipalUid(token, loadCachedTokens());
303
+ const view = await putProjectView(token, company.companyUid, projectId, {
304
+ name: opts.name?.trim() || source?.name || current.name,
305
+ ...(opts.description !== undefined
306
+ ? { description: opts.description }
307
+ : source?.description !== undefined
308
+ ? { description: source.description }
309
+ : { description: current.description }),
310
+ stories: source?.stories ?? current.stories,
311
+ repos: explicitRepos ?? source?.repos ?? current.repos,
312
+ ...(current.version !== undefined ? { version: current.version } : {}),
313
+ });
314
+ if (opts.json) {
315
+ console.log(JSON.stringify({ ok: true, action: "project.set", principalUid, ...view }, null, 2));
316
+ return;
317
+ }
318
+ console.log(`Work mesh: project ${view.projectId} set (${view.name}; ${view.stories.length} stories)`);
319
+ }
320
+ async function runProjectRegister(projectId, opts) {
321
+ if (opts.channel?.trim()) {
322
+ fail("--channel is not supported by the current Work Mesh registration API; omit it to let the server create or bind the project channel.");
323
+ }
324
+ const { token, company } = await withCompany(opts);
325
+ const principalUid = await resolveMeshPrincipalUid(token, loadCachedTokens());
326
+ const registration = await registerMeshProject(token, company.companyUid, projectId);
327
+ const boardState = "registered";
328
+ if (opts.json) {
329
+ console.log(JSON.stringify({ ok: true, action: "project.register", principalUid, boardState, ...registration }, null, 2));
330
+ return;
331
+ }
332
+ console.log(`Work mesh: project ${registration.projectId} registered (${boardState}; channel ${registration.channelId} ${registration.channelCreated ? "created" : "bound"})`);
126
333
  }
127
334
  async function runDoctor(opts) {
128
335
  if (opts.apply) {
@@ -131,6 +338,9 @@ async function runDoctor(opts) {
131
338
  }
132
339
  const token = await requireToken();
133
340
  const principalUid = await resolveMeshPrincipalUid(token, loadCachedTokens());
341
+ if (!principalUid) {
342
+ fail("Could not resolve the authenticated principal. Sign in again and retry `hq mesh doctor`.");
343
+ }
134
344
  const warmed = await warmMeshConversationCache(token, principalUid);
135
345
  if (opts.json) {
136
346
  console.log(JSON.stringify({ ok: true, action: "doctor", ...warmed }, null, 2));
@@ -346,19 +556,20 @@ async function runContextBackfillHeld(opts) {
346
556
  const limit = Number.isFinite(parsedLimit) && parsedLimit > 0 ? parsedLimit : 0;
347
557
  const companyHint = opts.company?.trim() || undefined;
348
558
  const useDeviceDefault = opts.useDeviceDefault === true || readDeviceConfig({ root }).repairHeldWithDefault === true;
349
- // Build the reconcile network seam exactly like `hq mesh context reconcile`.
350
- // Not needed in --dry-run (reconcile is never invoked then). Explicit types
351
- // because these are captured in the reconcile closure below (no control-flow
352
- // narrowing, unlike the inline reconcile handler).
559
+ // Build the membership seam for both modes; dry-run is read-only but must
560
+ // validate a locally resolved slug just as live reconciliation does.
561
+ // The remaining seams are captured in the reconcile closure below.
353
562
  let deliver;
354
563
  let fetchCandidates;
355
564
  let validateMembership;
356
565
  let offline = Boolean(opts.offline);
357
- if (!dryRun && !offline) {
566
+ if (!offline) {
358
567
  try {
359
568
  const token = await requireToken();
360
- deliver = createWorkSessionDeliverer({ token });
361
- fetchCandidates = createCandidatesFetcher({ token });
569
+ if (!dryRun) {
570
+ deliver = createWorkSessionDeliverer({ token });
571
+ fetchCandidates = createCandidatesFetcher({ token });
572
+ }
362
573
  validateMembership = async (candidate) => {
363
574
  const membership = await resolveActiveMembershipCompany(token, candidate);
364
575
  if (!membership)
@@ -383,6 +594,7 @@ async function runContextBackfillHeld(opts) {
383
594
  // so `--dry-run` and the real run report the same counts.
384
595
  remoteOwnerHint: companyHint || undefined,
385
596
  useDeviceDefault,
597
+ validateMembership,
386
598
  reconcile: (obs) => reconcileObservation(
387
599
  // Pass --company as a non-forcing hint (remoteOwnerSlug feeds
388
600
  // deterministic resolution BELOW the identity-file / device default,
@@ -410,6 +622,9 @@ async function runContextBackfillHeld(opts) {
410
622
  `${result.unresolved} unresolved, ` +
411
623
  `${result.errors} error(s).` +
412
624
  (dryRun ? " (dry-run: nothing written)" : ""));
625
+ for (const { sessionId, reason } of result.unresolvedReasons) {
626
+ console.log(`Unresolved ${sessionId}: ${reason}`);
627
+ }
413
628
  if (!dryRun && result.reconciled > 0) {
414
629
  console.log("Reconciled session state files were rewritten with a resolved company; " +
415
630
  "the mesh daemon's held retry will re-attribute and post those events.");
@@ -1075,9 +1290,32 @@ export function registerMeshCommand(program) {
1075
1290
  .option("--summary <text>", "Note text")).action((opts) => wrap(() => runEvent("note", opts))());
1076
1291
  addSharedFlags(mesh
1077
1292
  .command("story")
1078
- .description("PATCH one Board story status")
1293
+ .description("PATCH one Board story status and/or assignee")
1079
1294
  .requiredOption("--story <id>", "Story id, e.g. US-001")
1080
- .requiredOption("--status <status>", "queued|in_progress|review|done")).action((opts) => wrap(() => runStory(opts))());
1295
+ .option("--status <status>", "queued|in_progress|review|done")
1296
+ .option("--assignee <uid|me|none>", "Assign a person or agent uid, yourself, or clear")).action((opts) => wrap(() => runStory(opts))());
1297
+ const project = mesh
1298
+ .command("project")
1299
+ .description("Set a project Board view or register its project channel");
1300
+ project
1301
+ .command("set <projectId>")
1302
+ .description("PUT the project Board view from flags or a PRD JSON file")
1303
+ .requiredOption("--company <slug|uid>", "Company slug or cloud uid")
1304
+ .option("--name <name>", "Project name")
1305
+ .option("--description <text>", "Project description")
1306
+ .option("--stories-file <path>", "PRD JSON file with userStories")
1307
+ .option("--stories-json <json>", "Story array or JSON object with userStories")
1308
+ .option("--repos-json <json>", "Repository array, such as [{path,branch}]")
1309
+ .option("--repo <path[@branch]>", "Repository binding (repeatable)", collectRepeatable)
1310
+ .option("--json", "Print machine-readable JSON")
1311
+ .action((projectId, opts) => wrap(() => runProjectSet(projectId, opts))());
1312
+ project
1313
+ .command("register <projectId>")
1314
+ .description("POST project registration to create or bind its project channel and Board")
1315
+ .requiredOption("--company <slug|uid>", "Company slug or cloud uid")
1316
+ .option("--channel <channelId>", "Reserved: the current server chooses the project channel")
1317
+ .option("--json", "Print machine-readable JSON")
1318
+ .action((projectId, opts) => wrap(() => runProjectRegister(projectId, opts))());
1081
1319
  mesh
1082
1320
  .command("doctor")
1083
1321
  .description("Warm ~/.hq/work-mesh/cache from hq-pro (directory, inbox, pair DMs). Not `hq doctor`.")
@@ -1107,7 +1345,7 @@ export function registerMeshCommand(program) {
1107
1345
  context
1108
1346
  .command("backfill-held")
1109
1347
  .description("Reconcile ENDED sessions whose held events lack a company so the daemon re-attributes the backlog (explicit, opt-in; no fleet fan-out)")
1110
- .option("--dry-run", "Report what would be reconciled; write nothing, no network")
1348
+ .option("--dry-run", "Report what would be reconciled; write nothing or deliver events")
1111
1349
  .option("--offline", "Reconcile outbox-only (no candidates/register network)")
1112
1350
  .option("--limit <n>", "Cap sessions processed (default 0 = no cap)")
1113
1351
  .option("--company <slug>", "Optional non-forcing company hint")
@@ -1227,11 +1465,13 @@ export function registerMeshCommand(program) {
1227
1465
  daemon
1228
1466
  .command("run")
1229
1467
  .description("Start the single-instance daemon (pid lock, spool watch, presence)")
1230
- .action(() => wrap(() => runDaemonRun())());
1468
+ .option("--hq-root <path>", "HQ tree root (else HQ_ROOT / persisted trusted roots); the Windows task passes this explicitly")
1469
+ .action((opts) => wrap(() => runDaemonRun(opts))());
1231
1470
  daemon
1232
1471
  .command("install")
1233
- .description("Install LaunchAgent (macOS) or systemd user unit (Linux)")
1472
+ .description("Install LaunchAgent (macOS), systemd user unit (Linux), or Scheduled Task (Windows)")
1234
1473
  .option("--system-user <user>", "Write a root-owned unit template for agent boxes (e.g. ec2-user)")
1474
+ .option("--hq-root <path>", "HQ root to record for the service (else HQ_ROOT, else the nearest HQ root above cwd)")
1235
1475
  .option("--json", "Print machine-readable JSON")
1236
1476
  .action((opts) => wrap(() => runDaemonInstall(opts))());
1237
1477
  daemon
@@ -1250,19 +1490,24 @@ export function registerMeshCommand(program) {
1250
1490
  .option("--json", "Print machine-readable JSON")
1251
1491
  .action((opts) => wrap(() => runDaemonDoctorCmd(opts))());
1252
1492
  }
1253
- async function runDaemonRun() {
1254
- const handle = await runMeshDaemon();
1493
+ async function runDaemonRun(opts = {}) {
1494
+ const hqRoot = opts.hqRoot?.trim() || undefined;
1495
+ const handle = await runMeshDaemon(hqRoot ? { hqRoot } : {});
1255
1496
  // Stay alive until signal handlers call stop + exit.
1256
1497
  await new Promise(() => {
1257
1498
  void handle;
1258
1499
  });
1259
1500
  }
1260
1501
  async function runDaemonInstall(opts) {
1502
+ // Verified root only: a repo's own .claude/ must never become the daemon's
1503
+ // HQ root (that path is persisted as trusted and used for attribution).
1504
+ const hqRoot = resolveInstallHqRoot({ explicit: opts.hqRoot });
1261
1505
  const result = installDaemonService({
1262
1506
  platform: detectPlatform(),
1263
1507
  paths: buildInstallPaths({
1264
1508
  systemUser: opts.systemUser,
1265
- hqRoot: process.env.HQ_ROOT?.trim() || findHqRoot(),
1509
+ hqRoot: hqRoot.root,
1510
+ hqRootSource: hqRoot.source,
1266
1511
  }),
1267
1512
  systemUser: opts.systemUser,
1268
1513
  });
@@ -1273,10 +1518,15 @@ async function runDaemonInstall(opts) {
1273
1518
  platform: result.platform,
1274
1519
  dest: result.dest,
1275
1520
  message: result.message,
1521
+ hqRoot: hqRoot.root,
1522
+ hqRootSource: hqRoot.source,
1523
+ ...(result.notice ? { notice: result.notice } : {}),
1276
1524
  }, null, 2));
1277
1525
  return;
1278
1526
  }
1279
1527
  console.log(result.message);
1528
+ if (result.notice)
1529
+ console.log(result.notice);
1280
1530
  }
1281
1531
  async function runDaemonUninstall(opts) {
1282
1532
  const result = uninstallDaemonService({
@@ -1310,12 +1560,21 @@ async function runDaemonStatus(opts) {
1310
1560
  running: result.running ?? false,
1311
1561
  dest: result.dest,
1312
1562
  message: result.message,
1563
+ ...(result.lastResult !== undefined ? { lastResult: result.lastResult } : {}),
1564
+ ...(result.lastRunFailed !== undefined ? { lastRunFailed: result.lastRunFailed } : {}),
1565
+ ...(result.recordedHqRoot !== undefined ? { hqRoot: result.recordedHqRoot } : {}),
1566
+ ...(result.rootValid !== undefined ? { rootValid: result.rootValid } : {}),
1567
+ ...(result.notice ? { notice: result.notice } : {}),
1313
1568
  mqttState: state?.mqttState,
1314
1569
  presenceRefusal,
1315
1570
  }, null, 2));
1316
1571
  return;
1317
1572
  }
1318
1573
  console.log(result.message);
1574
+ if (result.recordedHqRoot)
1575
+ console.log(`hq root: ${result.recordedHqRoot}`);
1576
+ if (result.notice)
1577
+ console.log(result.notice);
1319
1578
  if (presenceRefusal) {
1320
1579
  console.log(`presence refusal: ${presenceRefusal.code} (HTTP ${presenceRefusal.status}); next retry at ${presenceRefusal.nextRetryAt}`);
1321
1580
  }
@@ -1324,7 +1583,11 @@ async function runDaemonStatus(opts) {
1324
1583
  }
1325
1584
  }
1326
1585
  async function runDaemonDoctorCmd(opts) {
1327
- const report = collectDaemonDoctor({ hqRoot: process.env.HQ_ROOT?.trim() || findHqRoot() });
1586
+ // Same predicate as install: a repo's .claude/ is not an HQ root. Doctor
1587
+ // only reads, so an unresolvable root is reported as absent, not fatal.
1588
+ const report = collectDaemonDoctor({
1589
+ hqRoot: process.env.HQ_ROOT?.trim() || findAncestorHqRoot(process.cwd()) || undefined,
1590
+ });
1328
1591
  if (opts.json) {
1329
1592
  console.log(JSON.stringify({ ok: !report.unhealthy, ...report }, null, 2));
1330
1593
  return;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Who a new company is created for, when `hq onboard create-company` is run
3
+ * without `--email` / `--person-name`.
4
+ *
5
+ * The signed-in account already carries both on its ID token. Asking the
6
+ * person to type their own name again — which is what the setup bot had to do,
7
+ * because `hq whoami` shows only the email — is a question HQ can answer
8
+ * itself. Explicit flags still win.
9
+ */
10
+ export interface CreateCompanyIdentity {
11
+ email: string;
12
+ personName: string;
13
+ }
14
+ export type CreateCompanyIdentityResult = {
15
+ ok: true;
16
+ identity: CreateCompanyIdentity;
17
+ } | {
18
+ ok: false;
19
+ message: string;
20
+ };
21
+ /** Pure: flags first, then the ID token's claims. */
22
+ export declare function resolveCreateCompanyIdentity(flags: {
23
+ email?: string;
24
+ personName?: string;
25
+ }, claims: Record<string, unknown>): CreateCompanyIdentityResult;
26
+ //# sourceMappingURL=onboard-create-identity.d.ts.map
@@ -0,0 +1,23 @@
1
+ function text(value) {
2
+ return typeof value === "string" ? value.trim() : "";
3
+ }
4
+ /** Pure: flags first, then the ID token's claims. */
5
+ export function resolveCreateCompanyIdentity(flags, claims) {
6
+ const email = text(flags.email) || text(claims.email);
7
+ const personName = text(flags.personName) ||
8
+ text(claims.name) ||
9
+ [text(claims.given_name), text(claims.family_name)].filter(Boolean).join(" ");
10
+ const missing = [];
11
+ if (!email)
12
+ missing.push("--email");
13
+ if (!personName)
14
+ missing.push("--person-name");
15
+ if (missing.length > 0) {
16
+ return {
17
+ ok: false,
18
+ message: `Your HQ sign-in does not say ${missing.length === 2 ? "your email or name" : missing[0] === "--email" ? "your email" : "your name"}. Pass ${missing.join(" and ")}.`,
19
+ };
20
+ }
21
+ return { ok: true, identity: { email, personName } };
22
+ }
23
+ //# sourceMappingURL=onboard-create-identity.js.map
@@ -20,11 +20,13 @@
20
20
  */
21
21
  import chalk from "chalk";
22
22
  import { runOnboardCli, readCheckpoint } from "../lib/onboarding/index.js";
23
- import { DEFAULT_HQ_ROOT, DEFAULT_VAULT_API_URL, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
23
+ import { DEFAULT_HQ_ROOT, DEFAULT_VAULT_API_URL, ensureCognitoIdToken, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
24
24
  import { createDefaultVaultClient } from "./cloud-provision.js";
25
25
  import { detectOnboardingIdentityMismatch } from "./onboard-identity-guard.js";
26
26
  import { planOnboardJoin } from "./onboard-join.js";
27
27
  import { buildCreateCompanyWarning } from "./onboard-warning.js";
28
+ import { resolveCreateCompanyIdentity } from "./onboard-create-identity.js";
29
+ import { peekIdToken } from "../utils/id-token.js";
28
30
  import { addBotToNewCompany, botToAddAfterCreateCompany } from "./onboard-bot-membership.js";
29
31
  // ---------------------------------------------------------------------------
30
32
  // Command registration
@@ -40,11 +42,18 @@ export function registerOnboardCommand(program) {
40
42
  "a same-named company makes a separate one you'd be alone in.")
41
43
  .requiredOption("--slug <slug>", "Company slug (used as bucket name suffix)")
42
44
  .requiredOption("--name <name>", "Company display name")
43
- .requiredOption("--email <email>", "Your email (must match Cognito sign-in)")
44
- .requiredOption("--person-name <name>", "Your display name")
45
+ .option("--email <email>", "Your email (default: your HQ sign-in's email; must match it)")
46
+ .option("--person-name <name>", "Your display name (default: the name on your HQ sign-in)")
45
47
  .option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
46
- .action(async (options) => {
48
+ .action(async (flags) => {
47
49
  try {
50
+ const idToken = await ensureCognitoIdToken();
51
+ const resolved = resolveCreateCompanyIdentity(flags, peekIdToken(idToken));
52
+ if (!resolved.ok) {
53
+ console.error(chalk.red(`\n✗ ${resolved.message}`));
54
+ process.exit(1);
55
+ }
56
+ const options = { ...flags, ...resolved.identity };
48
57
  console.log(chalk.bold(`\nHQ Onboard — Create Company`));
49
58
  console.log(` Company: ${options.name} (${options.slug})`);
50
59
  console.log(` Person: ${options.personName} <${options.email}>`);
@@ -15,6 +15,10 @@
15
15
  * - the final answer is posted only when it adds something: text already
16
16
  * posted (or about to be) is never repeated, including Grok's final result
17
17
  * that is just every message concatenated
18
+ * - an in-between message that asks the person something (it has a "?") is
19
+ * held, not posted: the model often asks, keeps working, then asks the same
20
+ * thing again in its final answer, and the person read the question twice.
21
+ * It goes out only if the turn ends without a final answer of its own
18
22
  *
19
23
  * Posts are sent strictly in order. A failed post is logged by the caller's
20
24
  * `send` and does not stop the turn.
@@ -46,6 +50,8 @@ export declare class ProgressPoster {
46
50
  private readonly workingNoticeMs;
47
51
  private readonly workingNoticeText;
48
52
  private pending;
53
+ /** In-between questions, held until the turn ends (see the header). */
54
+ private heldQuestions;
49
55
  /** Every message the model has said this turn (posted or pending), in order. */
50
56
  private readonly said;
51
57
  private flushTimer;
@@ -15,6 +15,10 @@
15
15
  * - the final answer is posted only when it adds something: text already
16
16
  * posted (or about to be) is never repeated, including Grok's final result
17
17
  * that is just every message concatenated
18
+ * - an in-between message that asks the person something (it has a "?") is
19
+ * held, not posted: the model often asks, keeps working, then asks the same
20
+ * thing again in its final answer, and the person read the question twice.
21
+ * It goes out only if the turn ends without a final answer of its own
18
22
  *
19
23
  * Posts are sent strictly in order. A failed post is logged by the caller's
20
24
  * `send` and does not stop the turn.
@@ -37,6 +41,8 @@ export class ProgressPoster {
37
41
  workingNoticeMs;
38
42
  workingNoticeText;
39
43
  pending = [];
44
+ /** In-between questions, held until the turn ends (see the header). */
45
+ heldQuestions = [];
40
46
  /** Every message the model has said this turn (posted or pending), in order. */
41
47
  said = [];
42
48
  flushTimer = null;
@@ -75,6 +81,10 @@ export class ProgressPoster {
75
81
  return;
76
82
  this.cancelNotice();
77
83
  this.said.push(t);
84
+ if (t.includes("?")) {
85
+ this.heldQuestions.push(t);
86
+ return;
87
+ }
78
88
  this.pending.push(t);
79
89
  this.scheduleFlush();
80
90
  }
@@ -97,8 +107,12 @@ export class ProgressPoster {
97
107
  this.cancelNotice();
98
108
  this.cancelFlush();
99
109
  const final = finalText.trim();
110
+ const held = this.heldQuestions;
111
+ this.heldQuestions = [];
100
112
  if (final && !this.repeatsSaid(final))
101
113
  this.pending.push(final);
114
+ else if (held.length > 0)
115
+ this.pending.push(...held);
102
116
  if (this.pending.length === 0 && this.delivered === 0 && !final)
103
117
  this.pending.push("(no reply)");
104
118
  this.flushNow();
@@ -114,6 +128,8 @@ export class ProgressPoster {
114
128
  this.finished = true;
115
129
  this.cancelNotice();
116
130
  this.cancelFlush();
131
+ this.pending.push(...this.heldQuestions);
132
+ this.heldQuestions = [];
117
133
  this.flushNow();
118
134
  return this.chain;
119
135
  }