@indigoai-us/hq-cli 5.345.123 → 5.345.124

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.
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Work Mesh team coordination (WMTC-7 / WMTC-8): story claims, overlap,
3
+ * project reassignment, assignee suggestions, and the presence roster.
4
+ *
5
+ * Every route here except the story PATCH is new on hq-pro. Callers treat a
6
+ * 404/405 without an error code as "this server does not have the route yet"
7
+ * and degrade to one plain line instead of failing.
8
+ */
9
+ export declare const STORY_ASSIGNEE_CONFLICT = "STORY_ASSIGNEE_CONFLICT";
10
+ /**
11
+ * True when the server does not know the route: 405, or a 404 that carries no
12
+ * error code. A coded 404 (for example PROJECT_NOT_FOUND) is a real answer.
13
+ */
14
+ export declare function isRouteUnavailable(err: unknown): boolean;
15
+ export declare function unavailableLine(feature: string): string;
16
+ export interface StoryAssigneeConflict {
17
+ code: typeof STORY_ASSIGNEE_CONFLICT;
18
+ error: string;
19
+ holderUid: string;
20
+ holderDisplayName?: string;
21
+ }
22
+ /** Extract a structured conflict from a 409 meshJson failure, if it is one. */
23
+ export declare function storyConflictFromError(err: unknown): StoryAssigneeConflict | null;
24
+ export declare function formatStoryConflictLine(storyId: string, conflict: StoryAssigneeConflict, names?: NameDirectory): string;
25
+ export type NameDirectory = Map<string, string>;
26
+ export interface DirectoryPerson {
27
+ uid: string;
28
+ displayName?: string;
29
+ email?: string;
30
+ }
31
+ export declare function peopleFromPayload(payload: unknown): DirectoryPerson[];
32
+ /** People known from the local contacts/directory cache. Never throws. */
33
+ export declare function loadCachedPeople(cacheRoot?: string): DirectoryPerson[];
34
+ export declare function loadNameDirectory(cacheRoot?: string): NameDirectory;
35
+ export declare function displayNameFor(uid: string, serverName: string | undefined, names: NameDirectory): string;
36
+ export type PersonResolution = {
37
+ ok: true;
38
+ uid: string;
39
+ displayName?: string;
40
+ } | {
41
+ ok: false;
42
+ reason: "none" | "ambiguous";
43
+ candidates: DirectoryPerson[];
44
+ };
45
+ /** Resolve a uid, email, or display name against a list of people. */
46
+ export declare function resolvePerson(query: string, people: DirectoryPerson[]): PersonResolution;
47
+ export interface MeshOverlap {
48
+ actorUid: string;
49
+ displayName?: string;
50
+ actorType?: string;
51
+ sessionId: string;
52
+ repo: string;
53
+ branch: string;
54
+ projectId?: string;
55
+ storyId?: string;
56
+ reasons: string[];
57
+ sharedFiles: string[];
58
+ lastSeenAt?: string;
59
+ }
60
+ export interface OverlapRequest {
61
+ companyUid: string;
62
+ sessionId: string;
63
+ repo: string;
64
+ branch: string;
65
+ projectId?: string;
66
+ storyId?: string;
67
+ files?: string[];
68
+ }
69
+ export declare const OVERLAP_MAX_FILES = 200;
70
+ export declare function postOverlap(token: string, req: OverlapRequest): Promise<MeshOverlap[]>;
71
+ export declare function formatOverlapLines(overlaps: MeshOverlap[], names: NameDirectory): string[];
72
+ export interface ReassignResult {
73
+ projectId: string;
74
+ ownerUid: string;
75
+ previousOwnerUid: string | null;
76
+ reassignedStories: string[];
77
+ }
78
+ export declare function postProjectReassign(token: string, projectId: string, body: {
79
+ companyUid: string;
80
+ toUid: string;
81
+ includeOpenStories?: boolean;
82
+ note?: string;
83
+ }): Promise<ReassignResult>;
84
+ export interface SuggestionCandidate {
85
+ actorUid: string;
86
+ displayName?: string;
87
+ actorType?: string;
88
+ score: number;
89
+ idle: boolean;
90
+ reasons: string[];
91
+ }
92
+ export declare function fetchStorySuggestions(token: string, companyUid: string, projectId: string, storyId: string): Promise<SuggestionCandidate[]>;
93
+ export declare function formatSuggestionLines(storyId: string, candidates: SuggestionCandidate[], names: NameDirectory): string[];
94
+ export interface RosterMember {
95
+ actorUid: string;
96
+ displayName?: string;
97
+ actorType?: string;
98
+ daemonLastSeenAt: string | null;
99
+ lastSessionAt: string | null;
100
+ boundSessions: number;
101
+ unboundSessions: number;
102
+ }
103
+ export declare function fetchRoster(token: string, companyUid: string): Promise<RosterMember[]>;
104
+ export declare function rosterFlags(member: RosterMember): string[];
105
+ export declare function formatRosterLines(members: RosterMember[], companyLabel: string, names: NameDirectory): string[];
106
+ //# sourceMappingURL=team-coordination.d.ts.map
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Work Mesh team coordination (WMTC-7 / WMTC-8): story claims, overlap,
3
+ * project reassignment, assignee suggestions, and the presence roster.
4
+ *
5
+ * Every route here except the story PATCH is new on hq-pro. Callers treat a
6
+ * 404/405 without an error code as "this server does not have the route yet"
7
+ * and degrade to one plain line instead of failing.
8
+ */
9
+ import * as fs from "node:fs";
10
+ import * as path from "node:path";
11
+ import { meshJson } from "./api.js";
12
+ import { meshCacheRoot } from "./cache.js";
13
+ export const STORY_ASSIGNEE_CONFLICT = "STORY_ASSIGNEE_CONFLICT";
14
+ function asRecord(value) {
15
+ return value && typeof value === "object" && !Array.isArray(value)
16
+ ? value
17
+ : null;
18
+ }
19
+ function str(value) {
20
+ return typeof value === "string" && value.trim() ? value : undefined;
21
+ }
22
+ function strArray(value) {
23
+ return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
24
+ }
25
+ /**
26
+ * True when the server does not know the route: 405, or a 404 that carries no
27
+ * error code. A coded 404 (for example PROJECT_NOT_FOUND) is a real answer.
28
+ */
29
+ export function isRouteUnavailable(err) {
30
+ const e = err;
31
+ if (!e || typeof e !== "object")
32
+ return false;
33
+ if (e.status === 405)
34
+ return true;
35
+ return e.status === 404 && !e.code;
36
+ }
37
+ export function unavailableLine(feature) {
38
+ return `${feature} is not available on this server yet`;
39
+ }
40
+ /** Extract a structured conflict from a 409 meshJson failure, if it is one. */
41
+ export function storyConflictFromError(err) {
42
+ const e = err;
43
+ if (!e || typeof e !== "object" || e.status !== 409 || e.code !== STORY_ASSIGNEE_CONFLICT)
44
+ return null;
45
+ const body = asRecord(e.body) ?? {};
46
+ const holderDisplayName = str(body.holderDisplayName);
47
+ return {
48
+ code: STORY_ASSIGNEE_CONFLICT,
49
+ error: str(body.error) ?? e.message,
50
+ holderUid: str(body.holderUid) ?? "",
51
+ ...(holderDisplayName ? { holderDisplayName } : {}),
52
+ };
53
+ }
54
+ export function formatStoryConflictLine(storyId, conflict, names) {
55
+ const name = conflict.holderDisplayName || names?.get(conflict.holderUid) || "another teammate";
56
+ const uid = conflict.holderUid ? ` (${conflict.holderUid})` : "";
57
+ return `Story ${storyId} is assigned to ${name}${uid}. Ask them to hand it off, or re-run with --force.`;
58
+ }
59
+ function readJson(file) {
60
+ try {
61
+ return JSON.parse(fs.readFileSync(file, "utf8"));
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }
67
+ export function peopleFromPayload(payload) {
68
+ const rec = asRecord(payload);
69
+ const rows = Array.isArray(payload)
70
+ ? payload
71
+ : [
72
+ ...(Array.isArray(rec?.contacts) ? rec.contacts : []),
73
+ ...(Array.isArray(rec?.rows) ? rec.rows : []),
74
+ ...(Array.isArray(rec?.members) ? rec.members : []),
75
+ ];
76
+ const out = [];
77
+ for (const row of rows) {
78
+ const r = asRecord(row);
79
+ if (!r)
80
+ continue;
81
+ const uid = str(r.personUid) ?? str(r.actorUid) ?? str(r.uid);
82
+ if (!uid || !/^(prs|agt)_/.test(uid))
83
+ continue;
84
+ const displayName = str(r.displayName);
85
+ const email = str(r.email);
86
+ out.push({ uid, ...(displayName ? { displayName } : {}), ...(email ? { email } : {}) });
87
+ }
88
+ return out;
89
+ }
90
+ /** People known from the local contacts/directory cache. Never throws. */
91
+ export function loadCachedPeople(cacheRoot = meshCacheRoot()) {
92
+ const people = new Map();
93
+ for (const dir of ["contacts", "directory"]) {
94
+ let files;
95
+ try {
96
+ files = fs.readdirSync(path.join(cacheRoot, dir)).filter((f) => f.endsWith(".json"));
97
+ }
98
+ catch {
99
+ continue;
100
+ }
101
+ for (const file of files) {
102
+ for (const person of peopleFromPayload(readJson(path.join(cacheRoot, dir, file)))) {
103
+ const prior = people.get(person.uid);
104
+ const displayName = prior?.displayName ?? person.displayName;
105
+ const email = prior?.email ?? person.email;
106
+ people.set(person.uid, {
107
+ uid: person.uid,
108
+ ...(displayName ? { displayName } : {}),
109
+ ...(email ? { email } : {}),
110
+ });
111
+ }
112
+ }
113
+ }
114
+ return [...people.values()];
115
+ }
116
+ export function loadNameDirectory(cacheRoot = meshCacheRoot()) {
117
+ const names = new Map();
118
+ for (const person of loadCachedPeople(cacheRoot)) {
119
+ if (person.displayName)
120
+ names.set(person.uid, person.displayName);
121
+ }
122
+ return names;
123
+ }
124
+ export function displayNameFor(uid, serverName, names) {
125
+ return serverName?.trim() || names.get(uid) || uid;
126
+ }
127
+ /** Resolve a uid, email, or display name against a list of people. */
128
+ export function resolvePerson(query, people) {
129
+ const q = query.trim();
130
+ if (/^(prs|agt)_/.test(q)) {
131
+ const known = people.find((p) => p.uid === q);
132
+ return { ok: true, uid: q, ...(known?.displayName ? { displayName: known.displayName } : {}) };
133
+ }
134
+ const lower = q.toLowerCase();
135
+ const exact = people.filter((p) => p.email?.toLowerCase() === lower || p.displayName?.toLowerCase() === lower);
136
+ const matches = exact.length
137
+ ? exact
138
+ : people.filter((p) => p.displayName?.toLowerCase().split(/\s+/)[0] === lower);
139
+ if (matches.length === 1) {
140
+ const m = matches[0];
141
+ return { ok: true, uid: m.uid, ...(m.displayName ? { displayName: m.displayName } : {}) };
142
+ }
143
+ return { ok: false, reason: matches.length ? "ambiguous" : "none", candidates: matches };
144
+ }
145
+ export const OVERLAP_MAX_FILES = 200;
146
+ export async function postOverlap(token, req) {
147
+ const body = {
148
+ companyUid: req.companyUid,
149
+ sessionId: req.sessionId,
150
+ repo: req.repo,
151
+ branch: req.branch,
152
+ };
153
+ if (req.projectId)
154
+ body.projectId = req.projectId;
155
+ if (req.storyId)
156
+ body.storyId = req.storyId;
157
+ if (req.files?.length)
158
+ body.files = req.files.slice(0, OVERLAP_MAX_FILES);
159
+ const data = asRecord(await meshJson(token, "/v1/work-mesh/overlap", { method: "POST", body }));
160
+ const rows = Array.isArray(data?.overlaps) ? data.overlaps : [];
161
+ return rows.flatMap((row) => {
162
+ const r = asRecord(row);
163
+ const actorUid = str(r?.actorUid);
164
+ if (!r || !actorUid)
165
+ return [];
166
+ const displayName = str(r.displayName);
167
+ const actorType = str(r.actorType);
168
+ const projectId = str(r.projectId);
169
+ const storyId = str(r.storyId);
170
+ const lastSeenAt = str(r.lastSeenAt);
171
+ return [{
172
+ actorUid,
173
+ ...(displayName ? { displayName } : {}),
174
+ ...(actorType ? { actorType } : {}),
175
+ sessionId: str(r.sessionId) ?? "",
176
+ repo: str(r.repo) ?? "",
177
+ branch: str(r.branch) ?? "",
178
+ ...(projectId ? { projectId } : {}),
179
+ ...(storyId ? { storyId } : {}),
180
+ reasons: strArray(r.reasons),
181
+ sharedFiles: strArray(r.sharedFiles),
182
+ ...(lastSeenAt ? { lastSeenAt } : {}),
183
+ }];
184
+ });
185
+ }
186
+ export function formatOverlapLines(overlaps, names) {
187
+ if (overlaps.length === 0)
188
+ return ["No overlapping work found."];
189
+ const lines = [`${overlaps.length} overlapping session${overlaps.length === 1 ? "" : "s"}:`];
190
+ for (const o of overlaps) {
191
+ const who = displayNameFor(o.actorUid, o.displayName, names);
192
+ const where = [
193
+ o.repo ? `${o.repo}@${o.branch}` : "",
194
+ o.projectId ? `project ${o.projectId}` : "",
195
+ o.storyId ? `story ${o.storyId}` : "",
196
+ ].filter(Boolean).join(", ");
197
+ lines.push(`- ${who} in session ${o.sessionId}: same ${o.reasons.join(" + ") || "work"}${where ? ` (${where})` : ""}`);
198
+ if (o.sharedFiles.length) {
199
+ const shown = o.sharedFiles.slice(0, 10);
200
+ const more = o.sharedFiles.length - shown.length;
201
+ lines.push(` shared files: ${shown.join(", ")}${more > 0 ? ` (+${more} more)` : ""}`);
202
+ }
203
+ }
204
+ return lines;
205
+ }
206
+ export async function postProjectReassign(token, projectId, body) {
207
+ const payload = { companyUid: body.companyUid, toUid: body.toUid };
208
+ if (body.includeOpenStories)
209
+ payload.includeOpenStories = true;
210
+ if (body.note)
211
+ payload.note = body.note;
212
+ const data = asRecord(await meshJson(token, `/v1/work-mesh/projects/${encodeURIComponent(projectId)}/reassign`, { method: "POST", body: payload }));
213
+ return {
214
+ projectId: str(data?.projectId) ?? projectId,
215
+ ownerUid: str(data?.ownerUid) ?? body.toUid,
216
+ previousOwnerUid: str(data?.previousOwnerUid) ?? null,
217
+ reassignedStories: strArray(data?.reassignedStories),
218
+ };
219
+ }
220
+ export async function fetchStorySuggestions(token, companyUid, projectId, storyId) {
221
+ const data = asRecord(await meshJson(token, `/v1/work-mesh/projects/${encodeURIComponent(projectId)}/stories/${encodeURIComponent(storyId)}/suggestions`, { query: { companyUid } }));
222
+ const rows = Array.isArray(data?.candidates) ? data.candidates : [];
223
+ return rows.flatMap((row) => {
224
+ const r = asRecord(row);
225
+ const actorUid = str(r?.actorUid);
226
+ if (!r || !actorUid)
227
+ return [];
228
+ const displayName = str(r.displayName);
229
+ const actorType = str(r.actorType);
230
+ return [{
231
+ actorUid,
232
+ ...(displayName ? { displayName } : {}),
233
+ ...(actorType ? { actorType } : {}),
234
+ score: typeof r.score === "number" ? r.score : 0,
235
+ idle: r.idle === true,
236
+ reasons: strArray(r.reasons),
237
+ }];
238
+ });
239
+ }
240
+ export function formatSuggestionLines(storyId, candidates, names) {
241
+ if (candidates.length === 0)
242
+ return [`No suggested assignees for ${storyId}.`];
243
+ const lines = [`Suggested assignees for ${storyId}:`];
244
+ candidates.forEach((c, i) => {
245
+ const who = displayNameFor(c.actorUid, c.displayName, names);
246
+ const why = c.reasons.length ? `: ${c.reasons.join("; ")}` : "";
247
+ lines.push(`${i + 1}. ${who} (${c.actorUid}) score ${c.score}${c.idle ? ", idle" : ""}${why}`);
248
+ });
249
+ return lines;
250
+ }
251
+ export async function fetchRoster(token, companyUid) {
252
+ const data = asRecord(await meshJson(token, "/v1/work-mesh/live/roster", { query: { companyUid } }));
253
+ const rows = Array.isArray(data?.members) ? data.members : [];
254
+ return rows.flatMap((row) => {
255
+ const r = asRecord(row);
256
+ const actorUid = str(r?.actorUid);
257
+ if (!r || !actorUid)
258
+ return [];
259
+ const displayName = str(r.displayName);
260
+ const actorType = str(r.actorType);
261
+ return [{
262
+ actorUid,
263
+ ...(displayName ? { displayName } : {}),
264
+ ...(actorType ? { actorType } : {}),
265
+ daemonLastSeenAt: str(r.daemonLastSeenAt) ?? null,
266
+ lastSessionAt: str(r.lastSessionAt) ?? null,
267
+ boundSessions: typeof r.boundSessions === "number" ? r.boundSessions : 0,
268
+ unboundSessions: typeof r.unboundSessions === "number" ? r.unboundSessions : 0,
269
+ }];
270
+ });
271
+ }
272
+ export function rosterFlags(member) {
273
+ const flags = [];
274
+ if (!member.daemonLastSeenAt)
275
+ flags.push("no daemon report");
276
+ if (member.unboundSessions > 0 && member.boundSessions === 0)
277
+ flags.push("only unbound sessions");
278
+ return flags;
279
+ }
280
+ export function formatRosterLines(members, companyLabel, names) {
281
+ const lines = [
282
+ `Work mesh roster: ${companyLabel}`,
283
+ "name\tdaemon last seen\tlast session\tbound\tunbound\tflags",
284
+ ];
285
+ for (const m of members) {
286
+ lines.push([
287
+ displayNameFor(m.actorUid, m.displayName, names),
288
+ m.daemonLastSeenAt ?? "never",
289
+ m.lastSessionAt ?? "never",
290
+ String(m.boundSessions),
291
+ String(m.unboundSessions),
292
+ rosterFlags(m).join(", ") || "-",
293
+ ].join("\t"));
294
+ }
295
+ return lines;
296
+ }
297
+ //# sourceMappingURL=team-coordination.js.map
@@ -0,0 +1,75 @@
1
+ /**
2
+ * WMTC-8 gap 7: resolve the project for an unbound (needs_project) session
3
+ * from local evidence only. No network, no subprocess, so a background hook
4
+ * refresh can call it cheaply. Every failure resolves to "no binding".
5
+ *
6
+ * Order:
7
+ * (a) a prior binding for the same repo+branch recorded locally
8
+ * (b) PRDs in the session's company whose repo AND branch match
9
+ * (c) a PRD matching the repo only, when it is the single candidate
10
+ */
11
+ import type { SessionStateFile } from "./state.js";
12
+ export interface GitLocation {
13
+ /** Absolute top-level of the checkout containing cwd. */
14
+ topLevel: string;
15
+ /** Main checkout top-level when cwd is a linked worktree. */
16
+ mainTopLevel?: string;
17
+ branch: string | null;
18
+ }
19
+ export interface AutoBindCandidate {
20
+ projectId: string;
21
+ prdPath: string;
22
+ matchedBranch: boolean;
23
+ }
24
+ export type AutoBindDecision = {
25
+ action: "noop";
26
+ reason: "already_bound" | "not_needs_project" | "no_session";
27
+ projectId?: string;
28
+ } | {
29
+ action: "bind";
30
+ projectId: string;
31
+ source: "history" | "repo_branch" | "repo_only";
32
+ candidates: AutoBindCandidate[];
33
+ } | {
34
+ action: "skip";
35
+ reason: "no_git" | "no_company" | "no_match" | "ambiguous";
36
+ candidates: AutoBindCandidate[];
37
+ };
38
+ /** Find the git checkout for cwd and its current branch without spawning git. */
39
+ export declare function readGitLocation(cwd: string): GitLocation | null;
40
+ /** Repo identities for the checkout: HQ-relative paths and absolute paths. */
41
+ export declare function repoIdentities(loc: GitLocation, hqRoot: string): string[];
42
+ /** PRDs under companies/<company>/projects/* that name this checkout. */
43
+ export declare function findRepoPrdCandidates(input: {
44
+ hqRoot: string;
45
+ companySlug: string;
46
+ identities: string[];
47
+ branch: string | null;
48
+ }): AutoBindCandidate[];
49
+ export declare function autoBindHistoryPath(workContextRoot: string): string;
50
+ export declare function lookupPriorBinding(input: {
51
+ workContextRoot: string;
52
+ companySlug: string;
53
+ identities: string[];
54
+ branch: string | null;
55
+ }): string | null;
56
+ /** Remember a binding for this repo+branch. Best-effort; never throws. */
57
+ export declare function recordPriorBinding(input: {
58
+ workContextRoot: string;
59
+ companySlug: string;
60
+ identities: string[];
61
+ branch: string | null;
62
+ projectId: string;
63
+ now?: Date;
64
+ }): void;
65
+ export declare function decideAutoBind(input: {
66
+ state: SessionStateFile | null;
67
+ cwd: string;
68
+ hqRoot: string;
69
+ workContextRoot: string;
70
+ companySlug?: string;
71
+ }): AutoBindDecision & {
72
+ git?: GitLocation;
73
+ identities?: string[];
74
+ };
75
+ //# sourceMappingURL=auto-bind.d.ts.map