@indigoai-us/hq-cli 5.152.1 → 5.153.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,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.153.0] — 2026-09-23
6
+
7
+ ### Fixed
8
+
9
+ - `hq mesh context prd-sync` rebinds a session to the PRD's project when that project is in the same company, then emits `task_status` against the new project so the Board and channel line land there. A PRD in another company is still skipped and does not rebind.
10
+
5
11
  ## [5.152.1] — 2026-09-23
6
12
 
7
13
  ### Fixed
@@ -44,7 +44,7 @@ import { readSessionState, updateSessionState } from "../lib/work-context/state.
44
44
  import { findLocalPrd, listNeedsProjectSessions } from "../lib/work-context/bind-best.js";
45
45
  import { bindCallingSession, formatSessionBoundLine } from "../lib/work-context/bind-calling-session.js";
46
46
  import { rebindNeedsProjectOnPrompt } from "../lib/work-context/prompt-rebind.js";
47
- import { planPrdSync } from "../lib/work-context/prd-sync.js";
47
+ import { planPrdSync, rebindSessionToProject } from "../lib/work-context/prd-sync.js";
48
48
  import { reserveSessionSeq } from "../lib/mesh/live/session-seq.js";
49
49
  import { WORK_CONTEXT_CONTRACT_VERSION } from "../lib/work-context/contract.js";
50
50
  import { loadObservationFromFile, markUntracked, parseObservationJson, reconcileObservation, } from "../lib/work-context/reconcile.js";
@@ -550,6 +550,9 @@ async function runPrdSync(opts) {
550
550
  fail("Usage: hq mesh context prd-sync --session <sid> --file <prd.json>");
551
551
  const root = workContextHomeRoot();
552
552
  const plan = planPrdSync({ sessionId, file, root });
553
+ if (plan.rebindTo) {
554
+ rebindSessionToProject({ sessionId, root, projectId: plan.rebindTo });
555
+ }
553
556
  if (!plan.changed) {
554
557
  const payload = { ok: true, action: "context.prd-sync", changed: 0, ...(plan.skip ? { skip: plan.skip } : {}) };
555
558
  if (opts.json)
@@ -570,7 +573,9 @@ async function runPrdSync(opts) {
570
573
  return;
571
574
  }
572
575
  // Attribution evidence: without company and project the server files these
573
- // as unassigned and the Board never moves.
576
+ // as unassigned and the Board never moves. After a same-company switch,
577
+ // this is the new project's id so ingest writes SESSION_STATE.projectId
578
+ // and bound-forward posts the channel line there.
574
579
  const bound = readSessionState(sessionId, root);
575
580
  const evidenceBase = {
576
581
  ...(bound?.companySlug ? { companySlug: bound.companySlug } : {}),
@@ -1,8 +1,11 @@
1
1
  /**
2
- * US-040: diff a bound project's prd.json and describe task_status events.
3
- * Planning never touches the network. The snapshot is written only after
4
- * the caller accepts the plan (commit), so a failed emit can retry.
2
+ * US-040 / F14: diff a bound project's prd.json and describe task_status events.
3
+ * A same-company PRD for a different project is a project switch (rebindTo).
4
+ * A PRD in another company is skipped. Planning never touches the network.
5
+ * The snapshot is written only after the caller accepts the plan (commit),
6
+ * so a failed emit can retry.
5
7
  */
8
+ import { type SessionStateFile } from "./state.js";
6
9
  export interface PrdStatusEvent {
7
10
  taskId: string;
8
11
  status: "done" | "in_progress";
@@ -13,14 +16,29 @@ export interface PrdSnapshot {
13
16
  status?: string;
14
17
  }>;
15
18
  seq: number;
19
+ /** Project the snapshot belongs to. A switch starts a fresh story set. */
20
+ projectId?: string;
16
21
  }
17
22
  export declare function prdSnapshotPath(sessionId: string, root: string): string;
18
23
  export declare function readPrdSnapshot(sessionId: string, root: string): PrdSnapshot | null;
24
+ /** `companies/<slug>/projects/<projectId>/prd.json` — nowhere else. */
25
+ export declare function prdPathIdentity(file: string): {
26
+ companySlug: string;
27
+ projectId: string;
28
+ } | null;
19
29
  export declare function prdBelongsToProject(file: string, projectId: string, companySlug: string | undefined): boolean;
30
+ export declare function rebindSessionToProject(input: {
31
+ sessionId: string;
32
+ root: string;
33
+ projectId: string;
34
+ now?: () => Date;
35
+ }): SessionStateFile | null;
20
36
  export interface PrdSyncPlan {
21
37
  changed: boolean;
22
38
  skip?: "no_state" | "not_bound" | "different_project" | "unreadable";
23
39
  events: PrdStatusEvent[];
40
+ /** Same-company project the session should switch to before emit. */
41
+ rebindTo?: string;
24
42
  /** Story most recently moved to in_progress, if any. */
25
43
  taskId?: string;
26
44
  seqStart: number;
@@ -1,11 +1,13 @@
1
1
  /**
2
- * US-040: diff a bound project's prd.json and describe task_status events.
3
- * Planning never touches the network. The snapshot is written only after
4
- * the caller accepts the plan (commit), so a failed emit can retry.
2
+ * US-040 / F14: diff a bound project's prd.json and describe task_status events.
3
+ * A same-company PRD for a different project is a project switch (rebindTo).
4
+ * A PRD in another company is skipped. Planning never touches the network.
5
+ * The snapshot is written only after the caller accepts the plan (commit),
6
+ * so a failed emit can retry.
5
7
  */
6
8
  import * as fs from "node:fs";
7
9
  import * as path from "node:path";
8
- import { readSessionState } from "./state.js";
10
+ import { readSessionState, updateSessionState } from "./state.js";
9
11
  export function prdSnapshotPath(sessionId, root) {
10
12
  return path.join(root, "sessions", `${sessionId}.prd-snapshot.json`);
11
13
  }
@@ -14,12 +16,33 @@ export function readPrdSnapshot(sessionId, root) {
14
16
  const parsed = JSON.parse(fs.readFileSync(prdSnapshotPath(sessionId, root), "utf8"));
15
17
  if (!parsed || typeof parsed !== "object" || !parsed.stories)
16
18
  return null;
17
- return { stories: parsed.stories, seq: typeof parsed.seq === "number" ? parsed.seq : 0 };
19
+ return {
20
+ stories: parsed.stories,
21
+ seq: typeof parsed.seq === "number" ? parsed.seq : 0,
22
+ ...(typeof parsed.projectId === "string" && parsed.projectId.trim()
23
+ ? { projectId: parsed.projectId.trim() }
24
+ : {}),
25
+ };
18
26
  }
19
27
  catch {
20
28
  return null;
21
29
  }
22
30
  }
31
+ /** `companies/<slug>/projects/<projectId>/prd.json` — nowhere else. */
32
+ export function prdPathIdentity(file) {
33
+ const parts = path.resolve(file).split(path.sep);
34
+ if (parts[parts.length - 1] !== "prd.json")
35
+ return null;
36
+ const projectsAt = parts.lastIndexOf("projects");
37
+ if (projectsAt < 2 || parts[projectsAt - 2] !== "companies" || parts.length !== projectsAt + 3) {
38
+ return null;
39
+ }
40
+ const companySlug = parts[projectsAt - 1];
41
+ const projectId = parts[projectsAt + 1];
42
+ if (!companySlug || !projectId)
43
+ return null;
44
+ return { companySlug, projectId };
45
+ }
23
46
  function writeSnapshot(sessionId, root, snapshot) {
24
47
  const dest = prdSnapshotPath(sessionId, root);
25
48
  fs.mkdirSync(path.dirname(dest), { recursive: true });
@@ -28,15 +51,29 @@ function writeSnapshot(sessionId, root, snapshot) {
28
51
  export function prdBelongsToProject(file, projectId, companySlug) {
29
52
  if (!companySlug)
30
53
  return false;
31
- const parts = path.resolve(file).split(path.sep);
32
- if (parts[parts.length - 1] !== "prd.json")
33
- return false;
34
- const projectsAt = parts.lastIndexOf("projects");
35
- return (projectsAt >= 2 &&
36
- parts[projectsAt - 2] === "companies" &&
37
- parts[projectsAt - 1] === companySlug &&
38
- parts[projectsAt + 1] === projectId &&
39
- parts.length === projectsAt + 3);
54
+ const identity = prdPathIdentity(file);
55
+ return Boolean(identity &&
56
+ identity.companySlug.toLowerCase() === companySlug.toLowerCase() &&
57
+ identity.projectId === projectId);
58
+ }
59
+ export function rebindSessionToProject(input) {
60
+ const nowIso = (input.now ?? (() => new Date()))().toISOString();
61
+ return updateSessionState(input.sessionId, input.root, (current) => {
62
+ if (!current)
63
+ return null;
64
+ if (current.projectId === input.projectId)
65
+ return current;
66
+ return {
67
+ ...current,
68
+ projectId: input.projectId,
69
+ taskId: undefined,
70
+ contextStatus: current.contextStatus === "bound" || current.contextStatus === "needs_task"
71
+ ? "needs_task"
72
+ : current.contextStatus,
73
+ projectBindReason: `prd-sync project switch to ${input.projectId}`,
74
+ updatedAt: nowIso,
75
+ };
76
+ });
40
77
  }
41
78
  function storiesFromPrd(file) {
42
79
  const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
@@ -79,8 +116,14 @@ export function planPrdSync(input) {
79
116
  if (!state.projectId || (state.contextStatus !== "bound" && state.contextStatus !== "needs_task")) {
80
117
  return empty("not_bound");
81
118
  }
82
- if (!prdBelongsToProject(input.file, state.projectId, state.companySlug))
119
+ const identity = prdPathIdentity(input.file);
120
+ if (!identity || !state.companySlug)
121
+ return empty("different_project");
122
+ if (identity.companySlug.toLowerCase() !== state.companySlug.toLowerCase()) {
83
123
  return empty("different_project");
124
+ }
125
+ const targetProjectId = identity.projectId;
126
+ const rebindTo = state.projectId !== targetProjectId ? targetProjectId : undefined;
84
127
  let stories;
85
128
  try {
86
129
  stories = storiesFromPrd(input.file);
@@ -88,7 +131,9 @@ export function planPrdSync(input) {
88
131
  catch {
89
132
  return empty("unreadable");
90
133
  }
91
- const previous = readPrdSnapshot(input.sessionId, input.root);
134
+ const previousRaw = readPrdSnapshot(input.sessionId, input.root);
135
+ const previousProject = previousRaw?.projectId ?? state.projectId;
136
+ const previous = previousProject === targetProjectId ? previousRaw : null;
92
137
  const before = previous?.stories ?? {};
93
138
  const events = [];
94
139
  let taskId;
@@ -108,10 +153,15 @@ export function planPrdSync(input) {
108
153
  taskId = story.id;
109
154
  }
110
155
  }
111
- const snapshot = { stories: nextStories, seq: (previous?.seq ?? 0) + events.length };
156
+ const snapshot = {
157
+ stories: nextStories,
158
+ seq: (previous?.seq ?? 0) + events.length,
159
+ projectId: targetProjectId,
160
+ };
112
161
  return {
113
162
  changed: events.length > 0,
114
163
  events,
164
+ ...(rebindTo ? { rebindTo } : {}),
115
165
  ...(taskId ? { taskId } : {}),
116
166
  seqStart: previous?.seq ?? 0,
117
167
  commit: (acceptedTaskIds) => {
@@ -134,6 +184,7 @@ export function planPrdSync(input) {
134
184
  writeSnapshot(input.sessionId, input.root, {
135
185
  stories,
136
186
  seq: (previous?.seq ?? 0) + advanced,
187
+ projectId: targetProjectId,
137
188
  });
138
189
  },
139
190
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.152.1",
3
+ "version": "5.153.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {