@indigoai-us/hq-cli 5.345.32 → 5.345.33

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 (46) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/command-catalog.generated.d.ts +92 -3
  3. package/dist/command-catalog.generated.js +111 -3
  4. package/dist/commands/db-dump.d.ts +8 -0
  5. package/dist/commands/db-dump.js +50 -0
  6. package/dist/commands/db-provision.d.ts +3 -10
  7. package/dist/commands/db-provision.js +26 -47
  8. package/dist/commands/db-remote-deps.d.ts +18 -0
  9. package/dist/commands/db-remote-deps.js +59 -0
  10. package/dist/commands/db-sql.d.ts +2 -1
  11. package/dist/commands/db-sql.js +30 -5
  12. package/dist/commands/db-usage.d.ts +7 -0
  13. package/dist/commands/db-usage.js +28 -0
  14. package/dist/commands/db.js +4 -0
  15. package/dist/commands/files.js +33 -5
  16. package/dist/commands/lanes-message.js +40 -8
  17. package/dist/commands/lanes-questions.d.ts +6 -1
  18. package/dist/commands/lanes-questions.js +19 -7
  19. package/dist/commands/lanes-resume.js +17 -6
  20. package/dist/commands/lanes-shared.d.ts +6 -1
  21. package/dist/commands/lanes.js +74 -1
  22. package/dist/commands/mesh.js +150 -26
  23. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +68 -604
  24. package/dist/hq-cloud-sync-runner/package.json +1 -1
  25. package/dist/lib/db/control-plane.d.ts +32 -0
  26. package/dist/lib/db/control-plane.js +50 -0
  27. package/dist/lib/db/redact.d.ts +4 -0
  28. package/dist/lib/db/redact.js +17 -0
  29. package/dist/lib/db/remote-engine.d.ts +67 -0
  30. package/dist/lib/db/remote-engine.js +165 -0
  31. package/dist/lib/db/usage-format.d.ts +4 -0
  32. package/dist/lib/db/usage-format.js +41 -0
  33. package/dist/lib/lanes/keep-alive.d.ts +12 -0
  34. package/dist/lib/lanes/keep-alive.js +25 -6
  35. package/dist/lib/mesh/api.d.ts +2 -0
  36. package/dist/lib/mesh/project-board.d.ts +17 -0
  37. package/dist/lib/mesh/project-board.js +28 -0
  38. package/dist/lib/mesh/story-status.d.ts +16 -0
  39. package/dist/lib/mesh/story-status.js +32 -7
  40. package/dist/utils/self-update.d.ts +2 -0
  41. package/dist/utils/self-update.js +61 -23
  42. package/dist/utils/version-check.d.ts +1 -1
  43. package/dist/utils/version-check.js +2 -2
  44. package/dist/utils/version-gate.d.ts +21 -14
  45. package/dist/utils/version-gate.js +119 -28
  46. package/package.json +4 -1
@@ -14,8 +14,8 @@ import chalk from "chalk";
14
14
  import { ensureCognitoToken, isExpiring, isMachineIdentity, loadCachedTokens, } from "../utils/cognito-session.js";
15
15
  import * as readline from "node:readline/promises";
16
16
  import { STORY_STATUSES, appendThreadEvent, addProjectMember, ensureProjectThread, eventPayload, getProjectView, listActiveMembershipCompanies, listActiveThreads, patchStory, putProjectView, registerMeshProject, resolveActiveMembershipCompany, resolveMeshPrincipalUid, warmMeshConversationCache, } from "../lib/mesh/api.js";
17
- import { closeSupersededProjectThread, ensureMissingBoardStories, ensureProjectBoard, boardStoryStatus, formatStoryStatusChanges, mergeProjectStories, prdSyncEventsThatChangeBoard, } from "../lib/mesh/project-board.js";
18
- import { mapSourceStoryStatus } from "../lib/mesh/story-status.js";
17
+ import { closeSupersededProjectThread, ensureMissingBoardStories, ensureProjectBoard, boardStoryStatus, formatStoryStatusChanges, mergeProjectStories, prdSyncEventsThatChangeBoard, reconcileProjectStories, } from "../lib/mesh/project-board.js";
18
+ import { ACCEPTED_LEDGER_STATES, mapSourceStoryStatus, UnknownProjectStoryStateError } from "../lib/mesh/story-status.js";
19
19
  import { buildPreflightFixtures } from "../lib/work-context/preflight-contract.js";
20
20
  import { createCandidatesFetcher, createMigratePoster, createOrganizePoster, createWorkSessionDeliverer, fetchCompanyLive, formatCompanyLiveTable, openMeshTransport, probeMigrationCapabilityForMemberships, requireToken, } from "../lib/mesh/client.js";
21
21
  import { clearDefaultCompany, effectiveUsageAttribution, isDefaultCompanyDisabled, readDeviceConfig, recordMigrationCapabilitySnapshot, setRepairHeldWithDefault, setDefaultCompany, setUsageAttribution, } from "../lib/work-context/config.js";
@@ -257,32 +257,43 @@ function projectStoriesFromJson(value, source) {
257
257
  if (!rawStories) {
258
258
  fail(`${source} must be a JSON story array or an object with userStories.`);
259
259
  }
260
- const stories = rawStories.map((raw, index) => {
260
+ const rejectedStories = [];
261
+ const stories = rawStories.flatMap((raw, index) => {
261
262
  const story = record(raw);
262
263
  const id = typeof story?.id === "string" ? story.id.trim() : "";
263
264
  const title = typeof story?.title === "string" ? story.title.trim() : "";
264
265
  if (!story || !id || !title) {
265
266
  fail(`${source} userStories[${index}] requires string id and title.`);
266
267
  }
267
- const status = mapSourceStoryStatus(story, `${source} userStories[${index}]`);
268
+ let status;
269
+ try {
270
+ status = mapSourceStoryStatus({ ...story, id }, `${source} userStories[${index}]`);
271
+ }
272
+ catch (err) {
273
+ if (!(err instanceof UnknownProjectStoryStateError))
274
+ throw err;
275
+ rejectedStories.push(err);
276
+ return [];
277
+ }
268
278
  const resolvedStatus = status || (story.passes === true ? "done" : undefined);
269
- return {
270
- id,
271
- title,
272
- ...(typeof story.description === "string" ? { description: story.description } : {}),
273
- ...(Array.isArray(story.acceptanceCriteria)
274
- ? {
275
- acceptanceCriteria: story.acceptanceCriteria.filter((item) => typeof item === "string"),
276
- }
277
- : {}),
278
- ...(resolvedStatus ? { status: resolvedStatus } : {}),
279
- // hq-pro makes passes mirror the Board status. A completed PRD story
280
- // without an explicit status must therefore become done.
281
- passes: resolvedStatus === "done",
282
- ...(typeof story.priority === "number" && Number.isFinite(story.priority)
283
- ? { priority: story.priority }
284
- : {}),
285
- };
279
+ return [{
280
+ id,
281
+ title,
282
+ ...(typeof story.description === "string" ? { description: story.description } : {}),
283
+ ...(Array.isArray(story.acceptanceCriteria)
284
+ ? {
285
+ acceptanceCriteria: story.acceptanceCriteria.filter((item) => typeof item === "string"),
286
+ }
287
+ : {}),
288
+ ...(resolvedStatus ? { status: resolvedStatus } : {}),
289
+ ...(typeof story.state === "string" ? { ledgerState: story.state.trim() } : {}),
290
+ // hq-pro makes passes mirror the Board status. A completed PRD story
291
+ // without an explicit status must therefore become done.
292
+ passes: resolvedStatus === "done",
293
+ ...(typeof story.priority === "number" && Number.isFinite(story.priority)
294
+ ? { priority: story.priority }
295
+ : {}),
296
+ }];
286
297
  });
287
298
  const metadata = record(plan?.metadata);
288
299
  const repos = metadata && Object.hasOwn(metadata, "repos")
@@ -292,12 +303,44 @@ function projectStoriesFromJson(value, source) {
292
303
  ...(typeof plan?.name === "string" ? { name: plan.name } : {}),
293
304
  ...(typeof plan?.description === "string" ? { description: plan.description } : {}),
294
305
  stories,
306
+ rejectedStories,
295
307
  ...(repos ? { repos } : {}),
296
308
  };
297
309
  }
298
- function parseProjectPlan(raw, source) {
310
+ function projectStateErrors(rejected) {
311
+ return rejected.map((error) => ({
312
+ code: error.code,
313
+ file: error.source.split(" userStories[")[0],
314
+ storyId: error.storyId,
315
+ field: "state",
316
+ value: error.value,
317
+ accepted: ACCEPTED_LEDGER_STATES,
318
+ }));
319
+ }
320
+ class RejectedProjectStoryStatesError extends Error {
321
+ rejected;
322
+ code = "PROJECT_STORY_STATE_INVALID";
323
+ constructor(rejected) {
324
+ super(rejected.map((error) => `${error.code}: ${error.message}`).join("\n"));
325
+ this.rejected = rejected;
326
+ this.name = "RejectedProjectStoryStatesError";
327
+ }
328
+ }
329
+ function failProjectStoryStates(rejected, json) {
330
+ if (json)
331
+ console.log(JSON.stringify({ ok: false, errors: projectStateErrors(rejected) }));
332
+ else
333
+ for (const error of rejected)
334
+ console.error(`${error.code}: ${error.message}`);
335
+ process.exitCode = 1;
336
+ }
337
+ function parseProjectPlan(raw, source, options = {}) {
299
338
  try {
300
- return projectStoriesFromJson(JSON.parse(raw), source);
339
+ const plan = projectStoriesFromJson(JSON.parse(raw), source);
340
+ if (plan.rejectedStories.length && !options.allowRejectedStories) {
341
+ throw new RejectedProjectStoryStatesError(plan.rejectedStories);
342
+ }
343
+ return plan;
301
344
  }
302
345
  catch (err) {
303
346
  if (err instanceof SyntaxError)
@@ -330,6 +373,10 @@ async function runProjectSet(projectId, opts) {
330
373
  : opts.storiesJson
331
374
  ? parseProjectPlan(opts.storiesJson, "--stories-json")
332
375
  : undefined;
376
+ if (source?.rejectedStories.length) {
377
+ failProjectStoryStates(source.rejectedStories, Boolean(opts.json));
378
+ return;
379
+ }
333
380
  const explicitRepos = opts.reposJson
334
381
  ? parseProjectReposJson(opts.reposJson, "--repos-json")
335
382
  : opts.repo?.length
@@ -479,8 +526,12 @@ async function runProjectMemberAdd(projectId, personUid, opts) {
479
526
  async function runProjectEnsure(projectId, opts) {
480
527
  if (!opts.storiesFile?.trim())
481
528
  fail("`--stories-file <prd.json>` is required.");
482
- const source = parseProjectPlan(readFileSync(opts.storiesFile, "utf8"), opts.storiesFile);
529
+ const source = parseProjectPlan(readFileSync(opts.storiesFile, "utf8"), opts.storiesFile, { allowRejectedStories: true });
483
530
  const { token, company } = await withCompany(opts);
531
+ if (source.rejectedStories.length && source.stories.length === 0) {
532
+ failProjectStoryStates(source.rejectedStories, Boolean(opts.json));
533
+ return;
534
+ }
484
535
  let current;
485
536
  try {
486
537
  current = await getProjectView(token, company.companyUid, projectId);
@@ -506,12 +557,13 @@ async function runProjectEnsure(projectId, opts) {
506
557
  }));
507
558
  if (opts.json) {
508
559
  console.log(JSON.stringify({
509
- ok: true,
560
+ ok: source.rejectedStories.length === 0,
510
561
  action: "project.ensure.dry-run",
511
562
  projectId,
512
563
  companyUid: company.companyUid,
513
564
  sourceStatuses,
514
565
  stories: boardStatuses,
566
+ ...(source.rejectedStories.length ? { errors: projectStateErrors(source.rejectedStories) } : {}),
515
567
  }, null, 2));
516
568
  }
517
569
  else {
@@ -523,6 +575,8 @@ async function runProjectEnsure(projectId, opts) {
523
575
  for (const story of boardStatuses)
524
576
  console.log(`${story.id}\t${story.status}`);
525
577
  }
578
+ if (source.rejectedStories.length)
579
+ process.exitCode = 1;
526
580
  return;
527
581
  }
528
582
  // Genesis thread, then the view, then register. Register returns 403
@@ -606,13 +660,74 @@ async function runProjectEnsure(projectId, opts) {
606
660
  channelId: registration.channelId,
607
661
  version: verified.version,
608
662
  stories: verified.stories,
663
+ ...(source.rejectedStories.length ? { ok: false, errors: projectStateErrors(source.rejectedStories) } : {}),
609
664
  };
610
665
  noteCallingSessionBound(payload.projectId, company);
611
666
  if (opts.json) {
612
667
  console.log(JSON.stringify(payload));
668
+ if (source.rejectedStories.length)
669
+ process.exitCode = 1;
613
670
  return;
614
671
  }
615
672
  console.log(`Work mesh: project ${payload.projectId} ensured (thread ${payload.threadId}, channel ${payload.channelId}, ${payload.stories.length} stories)`);
673
+ if (source.rejectedStories.length)
674
+ failProjectStoryStates(source.rejectedStories, false);
675
+ }
676
+ async function runProjectReconcile(projectId, opts) {
677
+ if (!opts.storiesFile?.trim())
678
+ fail("`--stories-file <prd.json>` is required.");
679
+ const source = parseProjectPlan(readFileSync(opts.storiesFile, "utf8"), opts.storiesFile, { allowRejectedStories: true });
680
+ const { token, company } = await withCompany(opts);
681
+ let current;
682
+ try {
683
+ current = await getProjectView(token, company.companyUid, projectId);
684
+ }
685
+ catch (err) {
686
+ if (httpStatus(err) !== 404)
687
+ throw err;
688
+ }
689
+ if (current && (current.companyUid !== company.companyUid || current.projectId !== projectId)) {
690
+ fail("Work mesh project reconcile: Board company or project id did not match the request.");
691
+ }
692
+ const reconciliation = reconcileProjectStories(source.stories, current?.stories ?? []);
693
+ const errors = [
694
+ ...projectStateErrors(source.rejectedStories),
695
+ ...(!current ? [{ code: "PROJECT_BOARD_NOT_FOUND", projectId }] : []),
696
+ ];
697
+ const mismatches = reconciliation.mismatches;
698
+ const clean = mismatches.length === 0 && errors.length === 0;
699
+ if (opts.json) {
700
+ console.log(JSON.stringify({
701
+ ok: clean,
702
+ action: "project.reconcile",
703
+ projectId,
704
+ companyUid: company.companyUid,
705
+ mismatches,
706
+ stateDriftCount: reconciliation.stateDriftCount,
707
+ ledgerStateUnsupported: reconciliation.ledgerStateUnsupported,
708
+ errors,
709
+ }, null, 2));
710
+ }
711
+ else if (clean) {
712
+ const note = reconciliation.ledgerStateUnsupported.length
713
+ ? `; Board does not return ledgerState for ${reconciliation.ledgerStateUnsupported.length} stories yet`
714
+ : "";
715
+ console.log(`Work mesh: project ${projectId} matches ${source.stories.length} ledger stories${note}`);
716
+ }
717
+ else {
718
+ for (const mismatch of mismatches) {
719
+ console.log(`${mismatch.storyId}\tledger=${mismatch.ledgerState ?? "<none>"}\texpected=${mismatch.expectedBoardStatus ?? "<none>"}\tboard=${mismatch.boardStatus ?? "<missing>"}\tboardLedger=${mismatch.boardLedgerState ?? "<unsupported>"}\tstateDrift=${mismatch.stateDrift}\tmissing=${mismatch.missingOnBoard}\textra=${mismatch.extraOnBoard}`);
720
+ }
721
+ if (reconciliation.ledgerStateUnsupported.length) {
722
+ console.log(`Board does not return ledgerState for ${reconciliation.ledgerStateUnsupported.length} stories: ${reconciliation.ledgerStateUnsupported.join(", ")}`);
723
+ }
724
+ if (!current)
725
+ console.error(`PROJECT_BOARD_NOT_FOUND: Board project ${projectId} was not found.`);
726
+ for (const error of source.rejectedStories)
727
+ console.error(`${error.code}: ${error.message}`);
728
+ }
729
+ if (!clean)
730
+ process.exitCode = 1;
616
731
  }
617
732
  async function runPrdSync(opts) {
618
733
  const sessionId = opts.session.trim();
@@ -689,6 +804,8 @@ async function runPrdSync(opts) {
689
804
  });
690
805
  }
691
806
  catch (err) {
807
+ if (err instanceof RejectedProjectStoryStatesError)
808
+ throw err;
692
809
  const reason = err instanceof Error ? err.message : String(err);
693
810
  console.error(`work-mesh: prd-sync ensure failed; emitting anyway (${reason.slice(0, 160)})`);
694
811
  }
@@ -828,8 +945,8 @@ async function runBindProject(target, opts) {
828
945
  }
829
946
  if (!current.companyUid)
830
947
  fail(`Session ${sessionId} has no company. Bind a company first.`);
831
- const { token, company } = await withCompany({ company: opts.company ?? current.companySlug });
832
948
  const plan = parseProjectPlan(readFileSync(prdPath, "utf8"), prdPath);
949
+ const { token, company } = await withCompany({ company: opts.company ?? current.companySlug });
833
950
  await ensureProjectBoard({
834
951
  token,
835
952
  company: {
@@ -2026,6 +2143,13 @@ export function registerMeshCommand(program, deps = {}) {
2026
2143
  .option("--dry-run", "Read the current Board and print computed story statuses without writing")
2027
2144
  .option("--json", "Print {projectId, threadId, channelId, version, stories}")
2028
2145
  .action((projectId, opts) => wrap(() => runProjectEnsure(projectId, opts))());
2146
+ project
2147
+ .command("reconcile <projectId>")
2148
+ .description("Read the project Board and report drift from a PRD ledger")
2149
+ .requiredOption("--company <slug|uid>", "Company slug or cloud uid")
2150
+ .requiredOption("--stories-file <path>", "PRD JSON file with userStories")
2151
+ .option("--json", "Print machine-readable mismatch records")
2152
+ .action((projectId, opts) => wrap(() => runProjectReconcile(projectId, opts))());
2029
2153
  project
2030
2154
  .command("register <projectId>")
2031
2155
  .description("Register an existing Board and bind its project channel; use project ensure to create or repair a Board")