@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.
@@ -61,6 +61,14 @@ export interface ContextResult {
61
61
  bindingEpisodeId?: string;
62
62
  clientOperationId: string;
63
63
  errorCode?: string;
64
+ /** Why a project was chosen, or why the session is asking. Local consumers only. */
65
+ projectBindReason?: string;
66
+ /** Ranked alternatives when classification is needs_project. */
67
+ projectCandidates?: Array<{
68
+ projectId: string;
69
+ title?: string;
70
+ score?: number;
71
+ }>;
64
72
  }
65
73
  export type NormalizeTaskIdResult = {
66
74
  ok: true;
@@ -69,6 +69,15 @@ export interface ReconcileDeps {
69
69
  * companyUid when local resolution only yielded a slug. Injectable for tests.
70
70
  */
71
71
  validateMembership?: (slug: string) => Promise<MembershipValidationResult> | MembershipValidationResult;
72
+ /**
73
+ * Register a Board and PUT the local PRD. Called only when online and the
74
+ * exact slug already has a local project folder.
75
+ */
76
+ ensureProjectBoard?: (input: {
77
+ companyUid: string;
78
+ projectId: string;
79
+ prdPath: string;
80
+ }) => Promise<void>;
72
81
  }
73
82
  export interface ReconcileOutcome {
74
83
  result: ContextResult;
@@ -9,6 +9,7 @@ import { invalidateDefaultCompanyMemberships, } from "./config.js";
9
9
  import { WORK_CONTEXT_CONTRACT_VERSION, normalizeTaskId, } from "./contract.js";
10
10
  import { EXIT_INVALID_IDENTITY, EXIT_NOT_TRACKING, EXIT_OK, InvalidDecisionOriginError, NotTrackingError, } from "./errors.js";
11
11
  import { decisionFromCandidates, } from "./organize.js";
12
+ import { refineProjectBinding } from "./bind-best.js";
12
13
  import { resolveProjectTask, shouldAskAfter } from "./project.js";
13
14
  import { deriveRemoteOwnerSlug, deriveRepoIdentityKey } from "./repo-remote.js";
14
15
  import { enqueueOutbox, listOutboxOperations, markOutboxAcked, markOutboxQuarantined, markOutboxQueued, replayOutbox, writeAckedRegisterState, } from "./outbox.js";
@@ -35,6 +36,10 @@ function resultOf(parts) {
35
36
  out.bindingEpisodeId = parts.bindingEpisodeId;
36
37
  if (parts.errorCode)
37
38
  out.errorCode = parts.errorCode;
39
+ if (parts.projectBindReason)
40
+ out.projectBindReason = parts.projectBindReason;
41
+ if (parts.projectCandidates)
42
+ out.projectCandidates = parts.projectCandidates;
38
43
  return out;
39
44
  }
40
45
  function parseUntrackedOrigin(obs) {
@@ -441,8 +446,77 @@ export async function reconcileObservation(obs, deps) {
441
446
  projectResolved = null;
442
447
  }
443
448
  }
444
- const projectId = projectResolved?.projectId;
445
- const taskId = projectResolved?.taskId;
449
+ let projectId = projectResolved?.projectId;
450
+ let taskId = projectResolved?.taskId;
451
+ let projectBindReason;
452
+ let projectCandidates;
453
+ let projectGate;
454
+ if (projectResolved &&
455
+ projectId &&
456
+ projectResolved.source !== "existing_binding" &&
457
+ obs.source !== "transcript" &&
458
+ hqRoot &&
459
+ resolvedCompany.slug) {
460
+ const refined = refineProjectBinding({
461
+ hint: projectId,
462
+ hqRoot,
463
+ companySlug: resolvedCompany.slug,
464
+ companyUid: resolvedCompany.uid,
465
+ repoPath: cwd,
466
+ env,
467
+ nowMs: nowFn().getTime(),
468
+ });
469
+ if (refined.action === "bind") {
470
+ if (refined.projectId !== projectId)
471
+ taskId = undefined;
472
+ projectId = refined.projectId;
473
+ projectBindReason = refined.reason;
474
+ diagnostics.push(`project bind: ${refined.reason}`);
475
+ }
476
+ else if (refined.action === "create") {
477
+ projectBindReason = refined.reason;
478
+ if (!deps.offline && resolvedCompany.uid && deps.ensureProjectBoard) {
479
+ try {
480
+ await deps.ensureProjectBoard({
481
+ companyUid: resolvedCompany.uid,
482
+ projectId,
483
+ prdPath: refined.prdPath,
484
+ });
485
+ diagnostics.push(`project board created for ${projectId}`);
486
+ }
487
+ catch (err) {
488
+ const message = err instanceof Error ? err.message : String(err);
489
+ diagnostics.push(`project board create failed: ${message}`);
490
+ projectBindReason = `Board create failed for ${projectId}: ${message}`;
491
+ projectCandidates = [{ projectId, title: projectId }];
492
+ projectGate = "choose";
493
+ projectId = undefined;
494
+ taskId = undefined;
495
+ projectResolved = null;
496
+ }
497
+ }
498
+ else {
499
+ diagnostics.push(`project bind: local PRD for ${projectId}; Board create waits for online reconcile`);
500
+ }
501
+ }
502
+ else if (refined.action === "ask") {
503
+ projectBindReason = refined.reason;
504
+ projectCandidates = refined.candidates;
505
+ projectGate = "choose";
506
+ projectId = undefined;
507
+ taskId = undefined;
508
+ projectResolved = null;
509
+ diagnostics.push(`project ask: ${refined.reason}`);
510
+ }
511
+ }
512
+ const bindResultFields = {
513
+ ...(projectBindReason ? { projectBindReason } : {}),
514
+ ...(projectCandidates ? { projectCandidates } : {}),
515
+ };
516
+ const bindStateFields = {
517
+ ...bindResultFields,
518
+ ...(projectGate ? { projectGate } : {}),
519
+ };
446
520
  // Trusted / deterministic auto-bind never asks.
447
521
  // Transcript-sourced sessions never ask (US-018).
448
522
  const fromTranscript = obs.source === "transcript";
@@ -457,7 +531,10 @@ export async function reconcileObservation(obs, deps) {
457
531
  (projectResolved.source === "deterministic_cwd" ||
458
532
  projectResolved.source === "deterministic_registry"));
459
533
  let classification;
460
- if (autoBound) {
534
+ if (projectGate === "choose") {
535
+ classification = "needs_project";
536
+ }
537
+ else if (autoBound) {
461
538
  classification = "bound";
462
539
  }
463
540
  else if (projectId && taskId) {
@@ -569,6 +646,7 @@ export async function reconcileObservation(obs, deps) {
569
646
  ...(typeof toolWrites === "number" ? { toolWrites } : {}),
570
647
  ...(hasTurn ? { hasTurn: true } : {}),
571
648
  ...(decision ? { decision } : {}),
649
+ ...bindStateFields,
572
650
  ...transcriptFields,
573
651
  }, prior), deps.root);
574
652
  const kind = registerClassification === "bound"
@@ -591,6 +669,7 @@ export async function reconcileObservation(obs, deps) {
591
669
  projectId,
592
670
  taskId,
593
671
  bindingEpisodeId: acknowledgedPrior.bindingEpisodeId,
672
+ ...bindResultFields,
594
673
  }),
595
674
  exitCode: EXIT_OK,
596
675
  diagnostics,
@@ -652,6 +731,7 @@ export async function reconcileObservation(obs, deps) {
652
731
  ...(hasTurn ? { hasTurn: true } : {}),
653
732
  ...(decision ? { decision } : {}),
654
733
  ...(companyConfidence ? { companyConfidence } : {}),
734
+ ...bindStateFields,
655
735
  ...transcriptFields,
656
736
  }, prior);
657
737
  statePath = writeSessionState(stateWrite, deps.root);
@@ -690,6 +770,7 @@ export async function reconcileObservation(obs, deps) {
690
770
  companySlug: resolvedCompany.slug,
691
771
  projectId,
692
772
  taskId,
773
+ ...bindResultFields,
693
774
  }),
694
775
  exitCode: EXIT_OK,
695
776
  diagnostics: [...diagnostics, "offline/local: operation remains queued"],
@@ -736,6 +817,7 @@ export async function reconcileObservation(obs, deps) {
736
817
  startedAt,
737
818
  ...(typeof toolWrites === "number" ? { toolWrites } : {}),
738
819
  ...(hasTurn ? { hasTurn: true } : {}),
820
+ ...bindStateFields,
739
821
  ...transcriptFields,
740
822
  },
741
823
  // Merge against the just-written projection rather than the stale
@@ -761,6 +843,7 @@ export async function reconcileObservation(obs, deps) {
761
843
  projectId,
762
844
  taskId,
763
845
  bindingEpisodeId: delivered.receiptId,
846
+ ...bindResultFields,
764
847
  }),
765
848
  exitCode: EXIT_OK,
766
849
  diagnostics: [...diagnostics, `acked receipt=${delivered.receiptId}`],
@@ -781,6 +864,7 @@ export async function reconcileObservation(obs, deps) {
781
864
  projectId,
782
865
  taskId,
783
866
  errorCode: delivered.code,
867
+ ...bindResultFields,
784
868
  }),
785
869
  exitCode: EXIT_OK,
786
870
  diagnostics: [...diagnostics, `transient ${delivered.code}; remains queued`],
@@ -801,6 +885,7 @@ export async function reconcileObservation(obs, deps) {
801
885
  projectId,
802
886
  taskId,
803
887
  errorCode: delivered.code,
888
+ ...bindResultFields,
804
889
  }),
805
890
  exitCode: EXIT_OK,
806
891
  diagnostics: [...diagnostics, `quarantined: ${delivered.code}`],
@@ -91,6 +91,19 @@ export interface SessionStateFile {
91
91
  transcriptMarker?: "from transcript";
92
92
  /** Historical repair under device-default evidence is intentionally weak. */
93
93
  companyConfidence?: "device_default";
94
+ /** Why the current project was chosen, or why the session is asking. */
95
+ projectBindReason?: string;
96
+ /** Ranked alternatives when the session is waiting for a project choice. */
97
+ projectCandidates?: Array<{
98
+ projectId: string;
99
+ title?: string;
100
+ score?: number;
101
+ }>;
102
+ /**
103
+ * When set, flush holds events instead of posting them.
104
+ * `choose` — the match was unclear. `missing_board` — the server 404'd the project.
105
+ */
106
+ projectGate?: "choose" | "missing_board";
94
107
  updatedAt: string;
95
108
  }
96
109
  /**
@@ -29,6 +29,9 @@ export const SESSION_STATE_ALLOWLIST = new Set([
29
29
  "localHarness",
30
30
  "transcriptMarker",
31
31
  "companyConfidence",
32
+ "projectBindReason",
33
+ "projectCandidates",
34
+ "projectGate",
32
35
  "updatedAt",
33
36
  ]);
34
37
  const SESSION_STATE_LOCK_TIMEOUT_MS = 5_000;
@@ -228,6 +231,12 @@ export function projectSessionState(state) {
228
231
  out.transcriptMarker = state.transcriptMarker;
229
232
  if (state.companyConfidence)
230
233
  out.companyConfidence = state.companyConfidence;
234
+ if (state.projectBindReason)
235
+ out.projectBindReason = state.projectBindReason;
236
+ if (state.projectCandidates?.length)
237
+ out.projectCandidates = state.projectCandidates;
238
+ if (state.projectGate)
239
+ out.projectGate = state.projectGate;
231
240
  return out;
232
241
  }
233
242
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.138.0",
3
+ "version": "5.140.0",
4
4
  "description": "HQ by Indigo management CLI \u2014 modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {