@indigoai-us/hq-cli 5.138.0 → 5.140.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.140.0] — 2026-09-23
6
+
7
+ ### Fixed
8
+
9
+ - `hq lanes monitor-check` now recognizes a live Claude Monitor task launched through the CLI's shell/node process wrapper, so active lanes are not reported as uncovered.
10
+ - A session hint now binds to the best existing project. An exact Board is used as-is. An exact local PRD gets a Board through the same genesis-thread, view, then register path as `project ensure`, and is bound. A single clear match is bound with a recorded reason. When nothing matches, or two candidates are close, the session stays `needs_project` and the start hook lists the candidates. `hq mesh context bind-project` applies the answer. A flush HTTP 404 downgrades that session, holds its events, and logs one line instead of dead-lettering them. `hq mesh doctor` lists `needs_project` sessions.
11
+
12
+ ## [5.139.0] — 2026-09-23
13
+
14
+ ### Added
15
+
16
+ - `hq mesh project ensure <projectId> --company <slug> --stories-file prd.json` registers a missing Board view, merges an existing one without replacing live story status or assignee, then checks the company, project id, story ids, and repo paths. `hq mesh project set --create` writes a view when the GET is a 404.
17
+
18
+ ### Fixed
19
+
20
+ - `hq report` now resolves report ids through local channel context and channel report lists instead of probing every channel with a report GET that can produce avoidable 404s.
21
+
5
22
  ## [5.138.0] — 2026-09-23
6
23
 
7
24
  ### Fixed
@@ -5708,11 +5708,36 @@ export declare const COMMAND_CATALOG: readonly [{
5708
5708
  }, {
5709
5709
  readonly flags: "--repo <path[@branch]>";
5710
5710
  readonly description: "Repository binding (repeatable)";
5711
+ }, {
5712
+ readonly flags: "--create";
5713
+ readonly description: "Create the Board view when the GET is a 404";
5711
5714
  }, {
5712
5715
  readonly flags: "--json";
5713
5716
  readonly description: "Print machine-readable JSON";
5714
5717
  }];
5715
5718
  readonly subcommands: readonly [];
5719
+ }, {
5720
+ readonly name: "ensure";
5721
+ readonly description: "Idempotently register a PRD on the Board and verify the view";
5722
+ readonly aliases: readonly [];
5723
+ readonly hidden: false;
5724
+ readonly usage: "[options] <projectId>";
5725
+ readonly arguments: readonly [{
5726
+ readonly name: "projectId";
5727
+ readonly required: true;
5728
+ readonly variadic: false;
5729
+ }];
5730
+ readonly options: readonly [{
5731
+ readonly flags: "--company <slug|uid>";
5732
+ readonly description: "Company slug or cloud uid";
5733
+ }, {
5734
+ readonly flags: "--stories-file <path>";
5735
+ readonly description: "PRD JSON file with userStories and metadata.repos";
5736
+ }, {
5737
+ readonly flags: "--json";
5738
+ readonly description: "Print {projectId, threadId, channelId, version, stories}";
5739
+ }];
5740
+ readonly subcommands: readonly [];
5716
5741
  }, {
5717
5742
  readonly name: "register";
5718
5743
  readonly description: "POST project registration to create or bind its project channel and Board";
@@ -5831,6 +5856,32 @@ export declare const COMMAND_CATALOG: readonly [{
5831
5856
  readonly description: "Print machine-readable JSON";
5832
5857
  }];
5833
5858
  readonly subcommands: readonly [];
5859
+ }, {
5860
+ readonly name: "bind-project";
5861
+ readonly description: "Bind this session to a project id, a numbered candidate, or new <slug>";
5862
+ readonly aliases: readonly [];
5863
+ readonly hidden: false;
5864
+ readonly usage: "[options] <target> [slug]";
5865
+ readonly arguments: readonly [{
5866
+ readonly name: "target";
5867
+ readonly required: true;
5868
+ readonly variadic: false;
5869
+ }, {
5870
+ readonly name: "slug";
5871
+ readonly required: false;
5872
+ readonly variadic: false;
5873
+ }];
5874
+ readonly options: readonly [{
5875
+ readonly flags: "--session <sid>";
5876
+ readonly description: "Canonical session id";
5877
+ }, {
5878
+ readonly flags: "--company <slug|uid>";
5879
+ readonly description: "Company override when creating a Board";
5880
+ }, {
5881
+ readonly flags: "--json";
5882
+ readonly description: "Print machine-readable JSON";
5883
+ }];
5884
+ readonly subcommands: readonly [];
5834
5885
  }, {
5835
5886
  readonly name: "organize";
5836
5887
  readonly description: "List or submit one-time project/task decisions (US-007B / US-005B)";
@@ -7367,6 +7367,10 @@ export const COMMAND_CATALOG = [
7367
7367
  "flags": "--repo <path[@branch]>",
7368
7368
  "description": "Repository binding (repeatable)"
7369
7369
  },
7370
+ {
7371
+ "flags": "--create",
7372
+ "description": "Create the Board view when the GET is a 404"
7373
+ },
7370
7374
  {
7371
7375
  "flags": "--json",
7372
7376
  "description": "Print machine-readable JSON"
@@ -7374,6 +7378,35 @@ export const COMMAND_CATALOG = [
7374
7378
  ],
7375
7379
  "subcommands": []
7376
7380
  },
7381
+ {
7382
+ "name": "ensure",
7383
+ "description": "Idempotently register a PRD on the Board and verify the view",
7384
+ "aliases": [],
7385
+ "hidden": false,
7386
+ "usage": "[options] <projectId>",
7387
+ "arguments": [
7388
+ {
7389
+ "name": "projectId",
7390
+ "required": true,
7391
+ "variadic": false
7392
+ }
7393
+ ],
7394
+ "options": [
7395
+ {
7396
+ "flags": "--company <slug|uid>",
7397
+ "description": "Company slug or cloud uid"
7398
+ },
7399
+ {
7400
+ "flags": "--stories-file <path>",
7401
+ "description": "PRD JSON file with userStories and metadata.repos"
7402
+ },
7403
+ {
7404
+ "flags": "--json",
7405
+ "description": "Print {projectId, threadId, channelId, version, stories}"
7406
+ }
7407
+ ],
7408
+ "subcommands": []
7409
+ },
7377
7410
  {
7378
7411
  "name": "register",
7379
7412
  "description": "POST project registration to create or bind its project channel and Board",
@@ -7526,6 +7559,40 @@ export const COMMAND_CATALOG = [
7526
7559
  ],
7527
7560
  "subcommands": []
7528
7561
  },
7562
+ {
7563
+ "name": "bind-project",
7564
+ "description": "Bind this session to a project id, a numbered candidate, or new <slug>",
7565
+ "aliases": [],
7566
+ "hidden": false,
7567
+ "usage": "[options] <target> [slug]",
7568
+ "arguments": [
7569
+ {
7570
+ "name": "target",
7571
+ "required": true,
7572
+ "variadic": false
7573
+ },
7574
+ {
7575
+ "name": "slug",
7576
+ "required": false,
7577
+ "variadic": false
7578
+ }
7579
+ ],
7580
+ "options": [
7581
+ {
7582
+ "flags": "--session <sid>",
7583
+ "description": "Canonical session id"
7584
+ },
7585
+ {
7586
+ "flags": "--company <slug|uid>",
7587
+ "description": "Company override when creating a Board"
7588
+ },
7589
+ {
7590
+ "flags": "--json",
7591
+ "description": "Print machine-readable JSON"
7592
+ }
7593
+ ],
7594
+ "subcommands": []
7595
+ },
7529
7596
  {
7530
7597
  "name": "organize",
7531
7598
  "description": "List or submit one-time project/task decisions (US-007B / US-005B)",
@@ -12,6 +12,7 @@ import chalk from "chalk";
12
12
  import { loadCachedTokens, isMachineIdentity } from "../utils/cognito-session.js";
13
13
  import * as readline from "node:readline/promises";
14
14
  import { STORY_STATUSES, appendThreadEvent, ensureProjectThread, eventPayload, getProjectView, listActiveMembershipCompanies, listActiveThreads, patchStory, putProjectView, registerMeshProject, resolveActiveMembershipCompany, resolveMeshPrincipalUid, warmMeshConversationCache, } from "../lib/mesh/api.js";
15
+ import { ensureProjectBoard } from "../lib/mesh/project-board.js";
15
16
  import { buildPreflightFixtures } from "../lib/work-context/preflight-contract.js";
16
17
  import { createCandidatesFetcher, createMigratePoster, createOrganizePoster, createWorkSessionDeliverer, fetchCompanyLive, formatCompanyLiveTable, openMeshTransport, probeMigrationCapabilityForMemberships, requireToken, } from "../lib/mesh/client.js";
17
18
  import { clearDefaultCompany, isDefaultCompanyDisabled, readDeviceConfig, recordMigrationCapabilitySnapshot, setRepairHeldWithDefault, setDefaultCompany, } from "../lib/work-context/config.js";
@@ -36,7 +37,9 @@ import { buildInstallPaths, collectDaemonDoctor, daemonServiceStatus, findAncest
36
37
  import { workContextRoot } from "../lib/work-context/paths.js";
37
38
  import { formatMigrateConfirmation, submitSessionMigration, } from "../lib/work-context/migrate.js";
38
39
  import { formatOrganizeList, prepareOrganizeDecision, settleOrganizeAskWithoutBind, submitOrganizeDecision, } from "../lib/work-context/organize.js";
39
- import { readSessionState } from "../lib/work-context/state.js";
40
+ import { readSessionState, updateSessionState } from "../lib/work-context/state.js";
41
+ import { findLocalPrd, listNeedsProjectSessions } from "../lib/work-context/bind-best.js";
42
+ import { WORK_CONTEXT_CONTRACT_VERSION } from "../lib/work-context/contract.js";
40
43
  import { loadObservationFromFile, markUntracked, parseObservationJson, reconcileObservation, } from "../lib/work-context/reconcile.js";
41
44
  import { resolveCompany } from "../lib/work-context/company.js";
42
45
  import { deriveRemoteOwnerSlug, deriveRepoIdentityKey, } from "../lib/work-context/repo-remote.js";
@@ -282,6 +285,33 @@ function parseProjectPlan(raw, source) {
282
285
  throw err;
283
286
  }
284
287
  }
288
+ function httpStatus(err) {
289
+ if (err && typeof err === "object" && "status" in err && typeof err.status === "number") {
290
+ return err.status;
291
+ }
292
+ return undefined;
293
+ }
294
+ function mergeProjectStories(current, planned) {
295
+ const currentById = new Map(current.map((story) => [story.id, story]));
296
+ const plannedIds = new Set(planned.map((story) => story.id));
297
+ const merged = planned.map((story) => {
298
+ const live = currentById.get(story.id);
299
+ if (!live)
300
+ return story;
301
+ const status = live.status || story.status;
302
+ return {
303
+ ...story,
304
+ ...(status ? { status } : {}),
305
+ passes: status === "done" ? true : typeof live.passes === "boolean" ? live.passes : story.passes,
306
+ ...(live.assigneeUid ? { assigneeUid: live.assigneeUid } : {}),
307
+ };
308
+ });
309
+ return [...merged, ...current.filter((story) => !plannedIds.has(story.id))];
310
+ }
311
+ function mergeProjectRepos(current, planned) {
312
+ const plannedPaths = new Set(planned.map((repo) => repo.path));
313
+ return [...planned, ...current.filter((repo) => !plannedPaths.has(repo.path))];
314
+ }
285
315
  async function runProjectSet(projectId, opts) {
286
316
  if (opts.storiesFile && opts.storiesJson) {
287
317
  fail("Pass only one of --stories-file or --stories-json.");
@@ -300,32 +330,63 @@ async function runProjectSet(projectId, opts) {
300
330
  ? opts.repo.map((repo, index) => parseProjectRepoSpec(repo, `--repo[${index}]`))
301
331
  : undefined;
302
332
  const { token, company } = await withCompany(opts);
303
- const current = await getProjectView(token, company.companyUid, projectId);
333
+ let current;
334
+ try {
335
+ current = await getProjectView(token, company.companyUid, projectId);
336
+ }
337
+ catch (err) {
338
+ if (httpStatus(err) === 404 && opts.create) {
339
+ if (!source && !opts.name?.trim()) {
340
+ fail("`--create` needs --name or a stories file when the Board view does not exist.");
341
+ }
342
+ current = undefined;
343
+ }
344
+ else {
345
+ throw err;
346
+ }
347
+ }
348
+ if (!current && !opts.create) {
349
+ fail(`Work mesh project ${projectId} has no Board view. Re-run with --create to add one.`);
350
+ }
304
351
  const principalUid = await resolveMeshPrincipalUid(token, loadCachedTokens());
305
352
  const PROJECT_CHANGED = "The Board changed while you were editing. Re-run to pick up the latest.";
306
353
  let view;
307
- try {
308
- view = await putProjectView(token, company.companyUid, projectId, {
309
- name: opts.name?.trim() || source?.name || current.name,
310
- ...(opts.description !== undefined
311
- ? { description: opts.description }
312
- : source?.description !== undefined
313
- ? { description: source.description }
314
- : { description: current.description }),
315
- stories: source?.stories ?? current.stories,
316
- repos: explicitRepos ?? source?.repos ?? current.repos,
317
- ...(current.version !== undefined ? { expectedVersion: current.version } : {}),
354
+ if (!current) {
355
+ const ensured = await ensureProjectBoard({
356
+ token,
357
+ company,
358
+ projectId,
359
+ name: opts.name?.trim() || source?.name || projectId,
360
+ description: opts.description !== undefined ? opts.description : source?.description ?? "",
361
+ stories: source?.stories ?? [],
362
+ repos: explicitRepos ?? source?.repos ?? [],
318
363
  });
364
+ view = ensured.view;
319
365
  }
320
- catch (err) {
321
- const conflict = err;
322
- if (conflict.status === 409 && conflict.code === "VERSION_CONFLICT") {
323
- if (opts.json) {
324
- console.log(JSON.stringify({ ok: false, code: "VERSION_CONFLICT", error: PROJECT_CHANGED }));
366
+ else {
367
+ try {
368
+ view = await putProjectView(token, company.companyUid, projectId, {
369
+ name: opts.name?.trim() || source?.name || current.name,
370
+ ...(opts.description !== undefined
371
+ ? { description: opts.description }
372
+ : source?.description !== undefined
373
+ ? { description: source.description }
374
+ : { description: current.description }),
375
+ stories: source?.stories ?? current.stories,
376
+ repos: explicitRepos ?? source?.repos ?? current.repos,
377
+ ...(current.version !== undefined ? { expectedVersion: current.version } : {}),
378
+ });
379
+ }
380
+ catch (err) {
381
+ const conflict = err;
382
+ if (conflict.status === 409 && conflict.code === "VERSION_CONFLICT") {
383
+ if (opts.json) {
384
+ console.log(JSON.stringify({ ok: false, code: "VERSION_CONFLICT", error: PROJECT_CHANGED }));
385
+ }
386
+ fail(PROJECT_CHANGED);
325
387
  }
326
- fail(PROJECT_CHANGED);
388
+ throw err;
327
389
  }
328
- throw err;
329
390
  }
330
391
  if (opts.json) {
331
392
  console.log(JSON.stringify({ ok: true, action: "project.set", principalUid, ...view }, null, 2));
@@ -347,6 +408,100 @@ async function runProjectRegister(projectId, opts) {
347
408
  }
348
409
  console.log(`Work mesh: project ${registration.projectId} registered (${boardState}; channel ${registration.channelId} ${registration.channelCreated ? "created" : "bound"})`);
349
410
  }
411
+ async function runProjectEnsure(projectId, opts) {
412
+ if (!opts.storiesFile?.trim())
413
+ fail("`--stories-file <prd.json>` is required.");
414
+ const source = parseProjectPlan(readFileSync(opts.storiesFile, "utf8"), opts.storiesFile);
415
+ const { token, company } = await withCompany(opts);
416
+ let current;
417
+ try {
418
+ current = await getProjectView(token, company.companyUid, projectId);
419
+ }
420
+ catch (err) {
421
+ if (httpStatus(err) !== 404)
422
+ throw err;
423
+ }
424
+ // Genesis thread, then the view, then register. Register returns 403
425
+ // PROJECT_NOT_IN_COMPANY until both exist. A missing view uses that chain
426
+ // from ensureProjectBoard; an existing view is merged, then registered.
427
+ let registration;
428
+ let verified;
429
+ if (!current) {
430
+ const ensured = await ensureProjectBoard({
431
+ token,
432
+ company,
433
+ projectId,
434
+ name: source.name || projectId,
435
+ description: source.description ?? "",
436
+ stories: source.stories,
437
+ repos: source.repos ?? [],
438
+ });
439
+ registration = ensured.registration;
440
+ verified = ensured.view;
441
+ }
442
+ else {
443
+ const genesis = await ensureProjectThread(token, company, projectId, {
444
+ summary: source.name || projectId,
445
+ });
446
+ if (!genesis.threadId)
447
+ fail("Work mesh did not return a threadId.");
448
+ {
449
+ const stories = mergeProjectStories(current.stories, source.stories);
450
+ const repos = mergeProjectRepos(current.repos, source.repos ?? []);
451
+ const name = source.name || current.name;
452
+ const description = source.description !== undefined ? source.description : current.description;
453
+ const changed = name !== current.name ||
454
+ description !== current.description ||
455
+ JSON.stringify(stories) !== JSON.stringify(current.stories) ||
456
+ JSON.stringify(repos) !== JSON.stringify(current.repos);
457
+ if (changed) {
458
+ await putProjectView(token, company.companyUid, projectId, {
459
+ name,
460
+ description,
461
+ stories,
462
+ repos,
463
+ ...(current.version !== undefined ? { expectedVersion: current.version } : {}),
464
+ });
465
+ }
466
+ }
467
+ registration = await registerMeshProject(token, company.companyUid, projectId);
468
+ if (!registration.threadId || !registration.channelId) {
469
+ fail("Work mesh registration did not return a threadId and channelId.");
470
+ }
471
+ verified = await getProjectView(token, company.companyUid, projectId);
472
+ }
473
+ if (verified.companyUid !== company.companyUid) {
474
+ fail(`Work mesh project ensure: company mismatch (expected ${company.companyUid}, got ${verified.companyUid}).`);
475
+ }
476
+ if (verified.projectId !== projectId) {
477
+ fail(`Work mesh project ensure: project id mismatch (expected ${projectId}, got ${verified.projectId}).`);
478
+ }
479
+ for (const story of source.stories) {
480
+ if (!verified.stories.some((row) => row.id === story.id)) {
481
+ fail(`Work mesh project ensure: missing story ${story.id}.`);
482
+ }
483
+ }
484
+ for (const repo of source.repos ?? []) {
485
+ if (!verified.repos.some((row) => row.path === repo.path)) {
486
+ fail(`Work mesh project ensure: missing repo ${repo.path}.`);
487
+ }
488
+ }
489
+ if (verified.version === undefined) {
490
+ fail("Work mesh project ensure: verified view has no version.");
491
+ }
492
+ const payload = {
493
+ projectId: verified.projectId,
494
+ threadId: registration.threadId,
495
+ channelId: registration.channelId,
496
+ version: verified.version,
497
+ stories: verified.stories,
498
+ };
499
+ if (opts.json) {
500
+ console.log(JSON.stringify(payload));
501
+ return;
502
+ }
503
+ console.log(`Work mesh: project ${payload.projectId} ensured (thread ${payload.threadId}, channel ${payload.channelId}, ${payload.stories.length} stories)`);
504
+ }
350
505
  async function runDoctor(opts) {
351
506
  if (opts.apply) {
352
507
  fail("`hq mesh doctor --apply` (paced Board PUTs from local prd.json) is not in the CLI yet.\n" +
@@ -358,13 +513,86 @@ async function runDoctor(opts) {
358
513
  fail("Could not resolve the authenticated principal. Sign in again and retry `hq mesh doctor`.");
359
514
  }
360
515
  const warmed = await warmMeshConversationCache(token, principalUid);
516
+ const needsProject = listNeedsProjectSessions(workContextHomeRoot());
361
517
  if (opts.json) {
362
- console.log(JSON.stringify({ ok: true, action: "doctor", ...warmed }, null, 2));
518
+ console.log(JSON.stringify({ ok: true, action: "doctor", ...warmed, needsProject }, null, 2));
363
519
  return;
364
520
  }
365
521
  console.log(`Work mesh cache warmed at ${warmed.cacheRoot}` +
366
522
  ` (directory=${warmed.directory} inbox=${warmed.inbox}` +
367
523
  ` contacts=${warmed.contacts} pair-threads=${warmed.threads})`);
524
+ if (needsProject.length === 0) {
525
+ console.log("needs_project sessions: none");
526
+ return;
527
+ }
528
+ console.log(`needs_project sessions: ${needsProject.length}`);
529
+ for (const row of needsProject) {
530
+ const choices = row.candidates.map((candidate) => candidate.projectId).join(", ") || "(none)";
531
+ console.log(`- ${row.sessionId}: ${row.reason ?? "no reason recorded"}; candidates: ${choices}`);
532
+ }
533
+ }
534
+ async function runBindProject(target, opts) {
535
+ const sessionId = opts.session.trim();
536
+ const root = workContextHomeRoot();
537
+ const current = readSessionState(sessionId, root);
538
+ if (!current)
539
+ fail(`No work-context state for session ${sessionId}.`);
540
+ let projectId = target.trim();
541
+ if (projectId === "new") {
542
+ const slug = opts.slug?.trim();
543
+ if (!slug)
544
+ fail("Usage: hq mesh context bind-project new <slug> --session <sid>");
545
+ const hqRoot = process.env.HQ_ROOT?.trim() || findHqRoot();
546
+ const prdPath = findLocalPrd(hqRoot, current.companySlug, slug);
547
+ if (!prdPath) {
548
+ fail(`No PRD for ${slug}. Run /prd before binding a new project.`);
549
+ }
550
+ if (!current.companyUid)
551
+ fail(`Session ${sessionId} has no company. Bind a company first.`);
552
+ const { token, company } = await withCompany({ company: opts.company ?? current.companySlug });
553
+ const plan = parseProjectPlan(readFileSync(prdPath, "utf8"), prdPath);
554
+ await ensureProjectBoard({
555
+ token,
556
+ company: {
557
+ companyUid: current.companyUid,
558
+ companySlug: current.companySlug || company.companySlug,
559
+ },
560
+ projectId: slug,
561
+ name: plan.name || slug,
562
+ description: plan.description ?? "",
563
+ stories: plan.stories,
564
+ repos: plan.repos ?? [],
565
+ });
566
+ projectId = slug;
567
+ }
568
+ else if (/^[1-9][0-9]*$/.test(projectId)) {
569
+ const index = Number(projectId) - 1;
570
+ const chosen = current.projectCandidates?.[index];
571
+ if (!chosen)
572
+ fail(`No candidate ${projectId} on session ${sessionId}.`);
573
+ projectId = chosen.projectId;
574
+ }
575
+ const updated = updateSessionState(sessionId, root, (state) => {
576
+ if (!state)
577
+ return null;
578
+ return {
579
+ ...state,
580
+ contractVersion: WORK_CONTEXT_CONTRACT_VERSION,
581
+ contextStatus: state.taskId ? "bound" : "needs_task",
582
+ projectId,
583
+ projectGate: undefined,
584
+ projectCandidates: undefined,
585
+ projectBindReason: `operator selected ${projectId}`,
586
+ updatedAt: new Date().toISOString(),
587
+ };
588
+ });
589
+ if (!updated)
590
+ fail(`No work-context state for session ${sessionId}.`);
591
+ if (opts.json) {
592
+ console.log(JSON.stringify({ ok: true, action: "context.bind-project", sessionId, projectId, contextStatus: updated.contextStatus }));
593
+ return;
594
+ }
595
+ console.log(`Work mesh: session ${sessionId} bound to project ${projectId}`);
368
596
  }
369
597
  async function runContextReconcile(opts) {
370
598
  if (!opts.observationFile && !opts.observationJson) {
@@ -455,6 +683,20 @@ async function runContextReconcile(opts) {
455
683
  fetchCandidates,
456
684
  offline,
457
685
  validateMembership,
686
+ ensureProjectBoard: token
687
+ ? async ({ companyUid, projectId, prdPath }) => {
688
+ const plan = parseProjectPlan(readFileSync(prdPath, "utf8"), prdPath);
689
+ await ensureProjectBoard({
690
+ token,
691
+ company: { companyUid, companySlug: companyUid },
692
+ projectId,
693
+ name: plan.name || projectId,
694
+ description: plan.description ?? "",
695
+ stories: plan.stories,
696
+ repos: plan.repos ?? [],
697
+ });
698
+ }
699
+ : undefined,
458
700
  });
459
701
  for (const line of outcome.diagnostics) {
460
702
  console.error(line);
@@ -1347,8 +1589,16 @@ export function registerMeshCommand(program) {
1347
1589
  .option("--stories-json <json>", "Story array or JSON object with userStories")
1348
1590
  .option("--repos-json <json>", "Repository array, such as [{path,branch}]")
1349
1591
  .option("--repo <path[@branch]>", "Repository binding (repeatable)", collectRepeatable)
1592
+ .option("--create", "Create the Board view when the GET is a 404")
1350
1593
  .option("--json", "Print machine-readable JSON")
1351
1594
  .action((projectId, opts) => wrap(() => runProjectSet(projectId, opts))());
1595
+ project
1596
+ .command("ensure <projectId>")
1597
+ .description("Idempotently register a PRD on the Board and verify the view")
1598
+ .requiredOption("--company <slug|uid>", "Company slug or cloud uid")
1599
+ .requiredOption("--stories-file <path>", "PRD JSON file with userStories and metadata.repos")
1600
+ .option("--json", "Print {projectId, threadId, channelId, version, stories}")
1601
+ .action((projectId, opts) => wrap(() => runProjectEnsure(projectId, opts))());
1352
1602
  project
1353
1603
  .command("register <projectId>")
1354
1604
  .description("POST project registration to create or bind its project channel and Board")
@@ -1392,6 +1642,15 @@ export function registerMeshCommand(program) {
1392
1642
  .option("--use-device-default", "Opt in to historical repair using the device default (lower-confidence attribution)")
1393
1643
  .option("--json", "Print machine-readable JSON")
1394
1644
  .action((opts) => wrap(() => runContextBackfillHeld(opts))());
1645
+ context
1646
+ .command("bind-project")
1647
+ .description("Bind this session to a project id, a numbered candidate, or new <slug>")
1648
+ .argument("<target>", "Project id, candidate number, or new")
1649
+ .argument("[slug]", "Project slug when target is new")
1650
+ .requiredOption("--session <sid>", "Canonical session id")
1651
+ .option("--company <slug|uid>", "Company override when creating a Board")
1652
+ .option("--json", "Print machine-readable JSON")
1653
+ .action((target, slug, opts) => wrap(() => runBindProject(target, { ...opts, slug }))());
1395
1654
  context
1396
1655
  .command("organize")
1397
1656
  .description("List or submit one-time project/task decisions (US-007B / US-005B)")
@@ -1,4 +1,3 @@
1
- /** `hq report` — safe channel-report lifecycle commands for bot runbooks. */
2
1
  import { Command } from "commander";
3
2
  import { buildReportTarball, deployOrgContext, type ChannelReport, type DeployAccessPolicy, type ReportDeployment, type ReportPatch } from "../lib/report-publish.js";
4
3
  /** Explicit context has priority; discovery is the authenticated fallback for normal CLI use. */