@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
|
package/dist/commands/mesh.js
CHANGED
|
@@ -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
|
-
*
|
|
4
|
-
*
|
|
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
|
-
*
|
|
4
|
-
*
|
|
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 {
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
|
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 = {
|
|
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
|
};
|