@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.
@@ -13,11 +13,11 @@ import { CLI_VERSION } from "../cli-version.js";
13
13
  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
- import { STORY_STATUSES, appendThreadEvent, addProjectMember, ensureProjectThread, eventPayload, getProjectView, listActiveMembershipCompanies, listActiveThreads, patchStory, putProjectView, registerMeshProject, resolveActiveMembershipCompany, resolveMeshPrincipalUid, warmMeshConversationCache, } from "../lib/mesh/api.js";
16
+ import { STORY_STATUSES, appendThreadEvent, addProjectMember, ensureProjectThread, eventPayload, getProjectView, listActiveMembershipCompanies, listActiveThreads, meshJson, patchStory, putProjectView, registerMeshProject, resolveActiveMembershipCompany, resolveMeshPrincipalUid, warmMeshConversationCache, } from "../lib/mesh/api.js";
17
17
  import { closeSupersededProjectThread, ensureMissingBoardStories, ensureProjectBoard, boardStoryStatus, formatStoryStatusChanges, mergeProjectStories, prdSyncEventsThatChangeBoard, reconcileProjectStories, } from "../lib/mesh/project-board.js";
18
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
- import { createCandidatesFetcher, createMigratePoster, createOrganizePoster, createWorkSessionDeliverer, fetchCompanyLive, formatCompanyLiveTable, openMeshTransport, probeMigrationCapabilityForMemberships, requireToken, } from "../lib/mesh/client.js";
20
+ import { createCandidatesFetcher, createMigratePoster, createOrganizePoster, createWorkSessionDeliverer, fetchCompanyLive, formatCompanyLiveTable, openMeshTransport, withParticipantDisplayNames, probeMigrationCapabilityForMemberships, requireToken, } from "../lib/mesh/client.js";
21
21
  import { clearDefaultCompany, effectiveUsageAttribution, isDefaultCompanyDisabled, readDeviceConfig, recordMigrationCapabilitySnapshot, setRepairHeldWithDefault, setDefaultCompany, setUsageAttribution, } from "../lib/work-context/config.js";
22
22
  import { ensureDefaultCompany } from "../lib/work-context/default-company.js";
23
23
  import { cognitoSubFromTokens } from "../lib/work-context/person-identity.js";
@@ -49,6 +49,8 @@ import { formatMigrateConfirmation, submitSessionMigration, } from "../lib/work-
49
49
  import { formatOrganizeList, prepareOrganizeDecision, settleOrganizeAskWithoutBind, submitOrganizeDecision, } from "../lib/work-context/organize.js";
50
50
  import { applyOperatorProjectBinding, readSessionState, updateSessionState, } from "../lib/work-context/state.js";
51
51
  import { findLocalPrd, listNeedsProjectSessions } from "../lib/work-context/bind-best.js";
52
+ import { decideAutoBind, readGitLocation, recordPriorBinding, repoIdentities, } from "../lib/work-context/auto-bind.js";
53
+ import { OVERLAP_MAX_FILES, fetchRoster, fetchStorySuggestions, formatOverlapLines, formatRosterLines, formatStoryConflictLine, formatSuggestionLines, isRouteUnavailable, loadCachedPeople, loadNameDirectory, peopleFromPayload, postOverlap, postProjectReassign, resolvePerson, storyConflictFromError, unavailableLine, } from "../lib/mesh/team-coordination.js";
52
54
  import { bindCallingSession, formatSessionBoundLine } from "../lib/work-context/bind-calling-session.js";
53
55
  import { rebindNeedsProjectOnPrompt } from "../lib/work-context/prompt-rebind.js";
54
56
  import { planPrdSync, rebindSessionToProject } from "../lib/work-context/prd-sync.js";
@@ -146,6 +148,32 @@ async function runEvent(verb, opts) {
146
148
  }
147
149
  console.log(`Work mesh: ${verb} ${ensured.threadId}${event.eventId ? ` (${event.eventId})` : ""}`);
148
150
  }
151
+ /** Exit code for a story already held by someone else (`--force` overrides). */
152
+ const STORY_CONFLICT_EXIT = 2;
153
+ function reportStoryConflict(action, storyId, conflict, json) {
154
+ if (json) {
155
+ console.log(JSON.stringify({ ok: false, action, storyId, conflict }, null, 2));
156
+ }
157
+ else {
158
+ console.error(formatStoryConflictLine(storyId, conflict, loadNameDirectory()));
159
+ }
160
+ process.exit(STORY_CONFLICT_EXIT);
161
+ }
162
+ /** Print the "route not on this server" line (or JSON) and return normally. */
163
+ function reportUnavailable(feature, action, json) {
164
+ if (json) {
165
+ console.log(JSON.stringify({ ok: false, unavailable: true, action, feature }));
166
+ return;
167
+ }
168
+ console.log(unavailableLine(feature));
169
+ }
170
+ async function requireAssignableSelf(token, verb) {
171
+ const resolved = await resolveMeshPrincipalUid(token, loadCachedTokens());
172
+ if (!resolved || resolved === "session") {
173
+ fail(`\`${verb}\` needs the signed-in person or agent. The synthetic session identity cannot be assigned. Sign in again and retry.`);
174
+ }
175
+ return resolved;
176
+ }
149
177
  async function runStory(opts) {
150
178
  const storyId = opts.story?.trim();
151
179
  const status = opts.status?.trim();
@@ -167,11 +195,7 @@ async function runStory(opts) {
167
195
  clearAssignee = true;
168
196
  }
169
197
  else if (assignee === "me") {
170
- const resolved = await resolveMeshPrincipalUid(token, loadCachedTokens());
171
- if (!resolved || resolved === "session") {
172
- fail("`--assignee me` needs the signed-in person or agent. The synthetic session identity cannot be assigned. Sign in again and retry `hq mesh story`.");
173
- }
174
- assigneeUid = resolved;
198
+ assigneeUid = await requireAssignableSelf(token, "--assignee me");
175
199
  }
176
200
  else if (assignee) {
177
201
  if (!/^(prs|agt)_/.test(assignee)) {
@@ -179,11 +203,21 @@ async function runStory(opts) {
179
203
  }
180
204
  assigneeUid = assignee;
181
205
  }
182
- const patched = await patchStory(token, company.companyUid, projectId, storyId, {
183
- ...(status ? { status } : {}),
184
- ...(assigneeUid ? { assigneeUid } : {}),
185
- ...(clearAssignee ? { clearAssignee: true } : {}),
186
- });
206
+ let patched;
207
+ try {
208
+ patched = await patchStory(token, company.companyUid, projectId, storyId, {
209
+ ...(status ? { status } : {}),
210
+ ...(assigneeUid ? { assigneeUid } : {}),
211
+ ...(clearAssignee ? { clearAssignee: true } : {}),
212
+ ...(opts.force ? { force: true } : {}),
213
+ });
214
+ }
215
+ catch (err) {
216
+ const conflict = storyConflictFromError(err);
217
+ if (conflict)
218
+ reportStoryConflict("story", storyId, conflict, opts.json);
219
+ throw err;
220
+ }
187
221
  if (opts.json) {
188
222
  console.log(JSON.stringify({ ok: true, action: "story", company, ...patched, storyId }, null, 2));
189
223
  return;
@@ -196,6 +230,34 @@ async function runStory(opts) {
196
230
  const statusPart = status ? `${storyId} → ${patched.status}` : storyId;
197
231
  console.log(`Work mesh: story ${statusPart}${assignment ? ` ${assignment}` : ""} (v${patched.version ?? "?"})`);
198
232
  }
233
+ /** `hq mesh claim`: assignee me + in_progress in one PATCH. */
234
+ async function runClaim(opts) {
235
+ const storyId = opts.story?.trim();
236
+ if (!storyId)
237
+ fail("claim requires --story <id>");
238
+ const projectId = requireProject(opts);
239
+ const { token, company } = await withCompany(opts);
240
+ const me = await requireAssignableSelf(token, "hq mesh claim");
241
+ let patched;
242
+ try {
243
+ patched = await patchStory(token, company.companyUid, projectId, storyId, {
244
+ status: "in_progress",
245
+ assigneeUid: me,
246
+ ...(opts.force ? { force: true } : {}),
247
+ });
248
+ }
249
+ catch (err) {
250
+ const conflict = storyConflictFromError(err);
251
+ if (conflict)
252
+ reportStoryConflict("claim", storyId, conflict, opts.json);
253
+ throw err;
254
+ }
255
+ if (opts.json) {
256
+ console.log(JSON.stringify({ ok: true, action: "claim", company, ...patched, storyId }, null, 2));
257
+ return;
258
+ }
259
+ console.log(`Work mesh: claimed ${storyId} (in_progress, assignee ${patched.assigneeUid ?? me}) (v${patched.version ?? "?"})`);
260
+ }
199
261
  function record(value) {
200
262
  return value && typeof value === "object" && !Array.isArray(value)
201
263
  ? value
@@ -769,9 +831,11 @@ async function runPrdSync(opts) {
769
831
  touchedPaths: [resolvePath(file)],
770
832
  };
771
833
  let toEmit = plan.events;
834
+ let liveStories = null;
772
835
  if (bound?.companyUid && bound.projectId) {
773
836
  try {
774
837
  const live = await getProjectView(token, bound.companyUid, bound.projectId);
838
+ liveStories = live.stories;
775
839
  toEmit = prdSyncEventsThatChangeBoard(plan.events, live.stories);
776
840
  }
777
841
  catch {
@@ -852,6 +916,16 @@ async function runPrdSync(opts) {
852
916
  }
853
917
  const advanced = events.flatMap((event) => (event.eventId && event.taskId && !blocked.has(event.eventId) ? [event.taskId] : []));
854
918
  plan.commit([...alreadyOnBoard, ...advanced]);
919
+ const claimed = bound?.companyUid && bound.projectId
920
+ ? await claimUnassignedInProgress({
921
+ token,
922
+ companyUid: bound.companyUid,
923
+ projectId: bound.projectId,
924
+ storyIds: toEmit.filter((event) => event.status === "in_progress" && advanced.includes(event.taskId))
925
+ .map((event) => event.taskId),
926
+ liveStories,
927
+ })
928
+ : [];
855
929
  if (plan.taskId && advanced.includes(plan.taskId)) {
856
930
  updateSessionState(sessionId, root, (current) => {
857
931
  if (!current)
@@ -865,11 +939,62 @@ async function runPrdSync(opts) {
865
939
  });
866
940
  }
867
941
  if (opts.json) {
868
- console.log(JSON.stringify({ ok: true, action: "context.prd-sync", changed: toEmit.length, taskId: plan.taskId }));
942
+ console.log(JSON.stringify({
943
+ ok: true,
944
+ action: "context.prd-sync",
945
+ changed: toEmit.length,
946
+ taskId: plan.taskId,
947
+ ...(claimed.length ? { claimed } : {}),
948
+ }));
869
949
  return;
870
950
  }
871
951
  console.log(`Work mesh: prd-sync emitted ${toEmit.length} task_status`);
872
952
  }
953
+ /**
954
+ * WMTC-7: a story prd-sync moves to in_progress with no Board assignee is
955
+ * claimed for the signed-in principal. A conflict means someone else holds
956
+ * it: warn and continue, never --force. Fail-open on every other error.
957
+ */
958
+ async function claimUnassignedInProgress(input) {
959
+ if (input.storyIds.length === 0)
960
+ return [];
961
+ const assigned = new Set((input.liveStories ?? [])
962
+ .filter((story) => {
963
+ const uid = story.assigneeUid;
964
+ return typeof uid === "string" && uid.trim() !== "";
965
+ })
966
+ .map((story) => story.id));
967
+ const pending = input.storyIds.filter((id) => !assigned.has(id));
968
+ if (pending.length === 0)
969
+ return [];
970
+ let me;
971
+ try {
972
+ me = await resolveMeshPrincipalUid(input.token, loadCachedTokens());
973
+ }
974
+ catch {
975
+ me = undefined;
976
+ }
977
+ if (!me || me === "session")
978
+ return [];
979
+ const claimed = [];
980
+ for (const storyId of pending) {
981
+ try {
982
+ await patchStory(input.token, input.companyUid, input.projectId, storyId, { assigneeUid: me });
983
+ claimed.push(storyId);
984
+ }
985
+ catch (err) {
986
+ const conflict = storyConflictFromError(err);
987
+ if (conflict) {
988
+ console.error(`work-mesh: ${formatStoryConflictLine(storyId, conflict, loadNameDirectory())}`);
989
+ }
990
+ else {
991
+ const reason = err instanceof Error ? err.message : String(err);
992
+ console.error(`work-mesh: prd-sync could not claim ${storyId} (${reason.slice(0, 160)})`);
993
+ }
994
+ }
995
+ }
996
+ return claimed;
997
+ }
873
998
  function noteCallingSessionBound(projectId, company) {
874
999
  try {
875
1000
  const bound = bindCallingSession({
@@ -927,6 +1052,27 @@ async function runDoctor(opts) {
927
1052
  console.log(`- ${row.sessionId}: ${row.reason ?? "no reason recorded"}; candidates: ${choices}`);
928
1053
  }
929
1054
  }
1055
+ /** Remember cwd's repo+branch → project so auto-bind can reuse it. Best-effort. */
1056
+ function rememberRepoBinding(root, companySlug, projectId) {
1057
+ if (!companySlug)
1058
+ return;
1059
+ try {
1060
+ const git = readGitLocation(process.cwd());
1061
+ if (!git)
1062
+ return;
1063
+ const hqRoot = process.env.HQ_ROOT?.trim() || findHqRoot();
1064
+ recordPriorBinding({
1065
+ workContextRoot: root,
1066
+ companySlug,
1067
+ identities: repoIdentities(git, hqRoot),
1068
+ branch: git.branch,
1069
+ projectId,
1070
+ });
1071
+ }
1072
+ catch {
1073
+ /* hint only */
1074
+ }
1075
+ }
930
1076
  async function runBindProject(target, opts) {
931
1077
  const sessionId = opts.session.trim();
932
1078
  const root = workContextHomeRoot();
@@ -971,6 +1117,7 @@ async function runBindProject(target, opts) {
971
1117
  const updated = updateSessionState(sessionId, root, (state) => state ? applyOperatorProjectBinding(state, projectId) : null);
972
1118
  if (!updated)
973
1119
  fail(`No work-context state for session ${sessionId}.`);
1120
+ rememberRepoBinding(root, updated.companySlug, projectId);
974
1121
  if (opts.json) {
975
1122
  console.log(JSON.stringify({ ok: true, action: "context.bind-project", sessionId, projectId, contextStatus: updated.contextStatus }));
976
1123
  return;
@@ -1770,18 +1917,261 @@ async function runSessionStatus(opts) {
1770
1917
  fail("`--company <slug>` is required for `hq mesh session status`");
1771
1918
  }
1772
1919
  const { token, company } = await withCompany({ company: opts.company });
1773
- const live = await fetchCompanyLive(token, company.companyUid);
1920
+ const names = loadNameDirectory();
1921
+ const live = withParticipantDisplayNames(await fetchCompanyLive(token, company.companyUid), names);
1774
1922
  const unattributedEvents = collectDaemonDoctor().unattributedEvents;
1775
1923
  if (opts.json) {
1776
1924
  console.log(JSON.stringify({ ok: true, action: "session-status", company, live, unattributedEvents }, null, 2));
1777
1925
  return;
1778
1926
  }
1779
- for (const line of formatCompanyLiveTable(live, company.companySlug || company.companyUid)) {
1927
+ for (const line of formatCompanyLiveTable(live, company.companySlug || company.companyUid, names)) {
1780
1928
  console.log(line);
1781
1929
  }
1782
1930
  console.log(`unattributed events: ${unattributedEvents.total} ` +
1783
1931
  `(spool=${unattributedEvents.spool} held=${unattributedEvents.held} dead-letter=${unattributedEvents.deadLetter})`);
1784
1932
  }
1933
+ function parseFilesJson(raw) {
1934
+ let parsed;
1935
+ try {
1936
+ parsed = JSON.parse(raw);
1937
+ }
1938
+ catch {
1939
+ fail("--files-json must be a JSON array of repo-relative paths");
1940
+ }
1941
+ if (!Array.isArray(parsed) || !parsed.every((f) => typeof f === "string")) {
1942
+ fail("--files-json must be a JSON array of repo-relative paths");
1943
+ }
1944
+ return parsed;
1945
+ }
1946
+ /** WMTC-8 gap 1: who else is working on this story, branch, or these files. */
1947
+ async function runOverlap(opts) {
1948
+ const sessionId = opts.session?.trim();
1949
+ const repo = opts.repo?.trim();
1950
+ const branch = opts.branch?.trim();
1951
+ if (!opts.company?.trim() || !sessionId || !repo || !branch) {
1952
+ fail("Usage: hq mesh overlap --company <slug> --session <sid> --repo <name> --branch <b>");
1953
+ }
1954
+ if (opts.file?.length && opts.filesJson)
1955
+ fail("Use --file or --files-json, not both.");
1956
+ const files = [...new Set((opts.filesJson ? parseFilesJson(opts.filesJson) : opts.file ?? [])
1957
+ .map((f) => f.trim()).filter(Boolean))];
1958
+ if (files.length > OVERLAP_MAX_FILES) {
1959
+ console.error(`work-mesh: sending the first ${OVERLAP_MAX_FILES} of ${files.length} files`);
1960
+ }
1961
+ const { token, company } = await withCompany({ company: opts.company });
1962
+ let overlaps;
1963
+ try {
1964
+ overlaps = await postOverlap(token, {
1965
+ companyUid: company.companyUid,
1966
+ sessionId,
1967
+ repo,
1968
+ branch,
1969
+ ...(opts.project?.trim() ? { projectId: opts.project.trim() } : {}),
1970
+ ...(opts.story?.trim() ? { storyId: opts.story.trim() } : {}),
1971
+ ...(files.length ? { files } : {}),
1972
+ });
1973
+ }
1974
+ catch (err) {
1975
+ if (isRouteUnavailable(err))
1976
+ return reportUnavailable("Overlap detection", "overlap", opts.json);
1977
+ throw err;
1978
+ }
1979
+ const names = loadNameDirectory();
1980
+ if (opts.json) {
1981
+ const named = overlaps.map((o) => {
1982
+ const displayName = o.displayName || names.get(o.actorUid);
1983
+ return displayName ? { ...o, displayName } : o;
1984
+ });
1985
+ console.log(JSON.stringify({ ok: true, action: "overlap", company, overlaps: named }, null, 2));
1986
+ return;
1987
+ }
1988
+ for (const line of formatOverlapLines(overlaps, names))
1989
+ console.log(line);
1990
+ }
1991
+ async function resolveReassignTarget(token, query) {
1992
+ let resolution = resolvePerson(query, loadCachedPeople());
1993
+ if (!resolution.ok && resolution.reason === "none") {
1994
+ try {
1995
+ resolution = resolvePerson(query, peopleFromPayload(await meshJson(token, "/v1/notify/contacts")));
1996
+ }
1997
+ catch {
1998
+ /* fall through to the cached answer */
1999
+ }
2000
+ }
2001
+ if (resolution.ok)
2002
+ return resolution;
2003
+ if (resolution.reason === "ambiguous") {
2004
+ const list = resolution.candidates
2005
+ .map((p) => `${p.displayName ?? p.uid} (${p.uid}${p.email ? `, ${p.email}` : ""})`)
2006
+ .join("; ");
2007
+ fail(`--to "${query}" matches more than one person: ${list}. Pass a uid or email.`);
2008
+ }
2009
+ fail(`--to "${query}" did not match anyone in the directory. Run \`hq mesh doctor\` to refresh it, or pass a uid.`);
2010
+ }
2011
+ /** WMTC-8 gap 3: move project ownership (and optionally open stories) to someone else. */
2012
+ async function runProjectReassign(projectId, opts) {
2013
+ const to = opts.to?.trim();
2014
+ if (!to)
2015
+ fail("project reassign requires --to <uid|email|name>");
2016
+ const { token, company } = await withCompany({ company: opts.company });
2017
+ const target = await resolveReassignTarget(token, to);
2018
+ let result;
2019
+ try {
2020
+ result = await postProjectReassign(token, projectId, {
2021
+ companyUid: company.companyUid,
2022
+ toUid: target.uid,
2023
+ ...(opts.includeOpenStories ? { includeOpenStories: true } : {}),
2024
+ ...(opts.note?.trim() ? { note: opts.note.trim() } : {}),
2025
+ });
2026
+ }
2027
+ catch (err) {
2028
+ if (isRouteUnavailable(err))
2029
+ return reportUnavailable("Project reassignment", "project.reassign", opts.json);
2030
+ throw err;
2031
+ }
2032
+ if (opts.json) {
2033
+ console.log(JSON.stringify({
2034
+ ok: true,
2035
+ action: "project.reassign",
2036
+ company,
2037
+ ...result,
2038
+ ...(target.displayName ? { ownerDisplayName: target.displayName } : {}),
2039
+ }, null, 2));
2040
+ return;
2041
+ }
2042
+ const names = loadNameDirectory();
2043
+ const owner = target.displayName || names.get(result.ownerUid) || result.ownerUid;
2044
+ const stories = result.reassignedStories.length
2045
+ ? `; reassigned ${result.reassignedStories.length} open stor${result.reassignedStories.length === 1 ? "y" : "ies"} (${result.reassignedStories.join(", ")})`
2046
+ : "";
2047
+ console.log(`Work mesh: project ${result.projectId} now owned by ${owner}${stories}`);
2048
+ }
2049
+ /** WMTC-8 gap 5: ranked assignee candidates for a story. */
2050
+ async function runSuggest(opts) {
2051
+ const storyId = opts.story?.trim();
2052
+ if (!storyId)
2053
+ fail("suggest requires --story <id>");
2054
+ const projectId = requireProject(opts);
2055
+ const { token, company } = await withCompany({ company: opts.company });
2056
+ let candidates;
2057
+ try {
2058
+ candidates = await fetchStorySuggestions(token, company.companyUid, projectId, storyId);
2059
+ }
2060
+ catch (err) {
2061
+ if (isRouteUnavailable(err))
2062
+ return reportUnavailable("Assignee suggestions", "suggest", opts.json);
2063
+ throw err;
2064
+ }
2065
+ const names = loadNameDirectory();
2066
+ if (opts.json) {
2067
+ const named = candidates.map((c) => {
2068
+ const displayName = c.displayName || names.get(c.actorUid);
2069
+ return displayName ? { ...c, displayName } : c;
2070
+ });
2071
+ console.log(JSON.stringify({ ok: true, action: "suggest", company, projectId, storyId, candidates: named }, null, 2));
2072
+ return;
2073
+ }
2074
+ for (const line of formatSuggestionLines(storyId, candidates, names))
2075
+ console.log(line);
2076
+ }
2077
+ /** WMTC-8 gap 6: who has a daemon reporting and who only has unbound sessions. */
2078
+ async function runRoster(opts) {
2079
+ if (!opts.company?.trim())
2080
+ fail("`--company <slug>` is required for `hq mesh roster`");
2081
+ const { token, company } = await withCompany({ company: opts.company });
2082
+ let members;
2083
+ try {
2084
+ members = await fetchRoster(token, company.companyUid);
2085
+ }
2086
+ catch (err) {
2087
+ if (isRouteUnavailable(err))
2088
+ return reportUnavailable("The presence roster", "roster", opts.json);
2089
+ throw err;
2090
+ }
2091
+ const names = loadNameDirectory();
2092
+ if (opts.json) {
2093
+ const named = members.map((m) => {
2094
+ const displayName = m.displayName || names.get(m.actorUid);
2095
+ return displayName ? { ...m, displayName } : m;
2096
+ });
2097
+ console.log(JSON.stringify({ ok: true, action: "roster", company, members: named }, null, 2));
2098
+ return;
2099
+ }
2100
+ for (const line of formatRosterLines(members, company.companySlug || company.companyUid, names)) {
2101
+ console.log(line);
2102
+ }
2103
+ }
2104
+ /**
2105
+ * WMTC-8 gap 7: bind a needs_project session from local repo/branch evidence.
2106
+ * Runs from a background hook refresh, so it is local-only and fail-open:
2107
+ * any unexpected error prints one line and exits 0.
2108
+ */
2109
+ async function runContextAutoBind(opts) {
2110
+ const sessionId = opts.session?.trim();
2111
+ if (!sessionId)
2112
+ fail("Usage: hq mesh context auto-bind --session <sid>");
2113
+ const out = (payload, line) => {
2114
+ if (opts.json)
2115
+ console.log(JSON.stringify({ action: "context.auto-bind", sessionId, ...payload }));
2116
+ else
2117
+ console.log(line);
2118
+ };
2119
+ try {
2120
+ const root = workContextHomeRoot();
2121
+ const hqRoot = process.env.HQ_ROOT?.trim() || findHqRoot();
2122
+ const state = readSessionState(sessionId, root);
2123
+ const decision = decideAutoBind({
2124
+ state,
2125
+ cwd: opts.cwd?.trim() || process.cwd(),
2126
+ hqRoot,
2127
+ workContextRoot: root,
2128
+ ...(opts.company?.trim() ? { companySlug: opts.company.trim() } : {}),
2129
+ });
2130
+ const candidates = "candidates" in decision
2131
+ ? decision.candidates.map((c) => ({ projectId: c.projectId, prdPath: c.prdPath, matchedBranch: c.matchedBranch }))
2132
+ : [];
2133
+ if (decision.action === "noop") {
2134
+ out({ ok: true, bound: false, reason: decision.reason, ...(decision.projectId ? { projectId: decision.projectId } : {}) }, decision.reason === "already_bound"
2135
+ ? `Work mesh: session ${sessionId} already bound to ${decision.projectId}`
2136
+ : `Work mesh: auto-bind skipped (${decision.reason})`);
2137
+ return;
2138
+ }
2139
+ if (decision.action === "skip") {
2140
+ const list = candidates.map((c) => c.projectId).join(", ");
2141
+ out({ ok: true, bound: false, reason: decision.reason, candidates }, `Work mesh: auto-bind did not bind (${decision.reason})${list ? `; candidates: ${list}` : ""}`);
2142
+ return;
2143
+ }
2144
+ if (opts.dryRun) {
2145
+ out({ ok: true, bound: false, dryRun: true, projectId: decision.projectId, source: decision.source, candidates }, `Work mesh: would bind session ${sessionId} to ${decision.projectId} (${decision.source})`);
2146
+ return;
2147
+ }
2148
+ const updated = updateSessionState(sessionId, root, (current) => current && current.contextStatus === "needs_project"
2149
+ ? applyOperatorProjectBinding(current, decision.projectId)
2150
+ : current);
2151
+ const companySlug = opts.company?.trim() || state?.companySlug;
2152
+ if (companySlug && decision.git && decision.identities) {
2153
+ recordPriorBinding({
2154
+ workContextRoot: root,
2155
+ companySlug,
2156
+ identities: decision.identities,
2157
+ branch: decision.git.branch,
2158
+ projectId: decision.projectId,
2159
+ });
2160
+ }
2161
+ out({
2162
+ ok: true,
2163
+ bound: updated?.projectId === decision.projectId,
2164
+ projectId: decision.projectId,
2165
+ source: decision.source,
2166
+ contextStatus: updated?.contextStatus,
2167
+ candidates,
2168
+ }, `Work mesh: session ${sessionId} bound to project ${decision.projectId} (${decision.source})`);
2169
+ }
2170
+ catch (err) {
2171
+ const reason = err instanceof Error ? err.message : String(err);
2172
+ out({ ok: false, bound: false, reason: "error", error: reason.slice(0, 200) }, `work-mesh: auto-bind failed open (${reason.slice(0, 160)})`);
2173
+ }
2174
+ }
1785
2175
  const HARNESSES = new Set([
1786
2176
  "claude-code",
1787
2177
  "codex",
@@ -2116,7 +2506,40 @@ export function registerMeshCommand(program, deps = {}) {
2116
2506
  .description("PATCH one Board story status and/or assignee")
2117
2507
  .requiredOption("--story <id>", "Story id, e.g. US-001")
2118
2508
  .option("--status <status>", "queued|in_progress|review|done")
2119
- .option("--assignee <uid|me|none>", "Assign a person or agent uid, yourself, or clear")).action((opts) => wrap(() => runStory(opts))());
2509
+ .option("--assignee <uid|me|none>", "Assign a person or agent uid, yourself, or clear")
2510
+ .option("--force", "Override another holder's assignment")).action((opts) => wrap(() => runStory(opts))());
2511
+ addSharedFlags(mesh
2512
+ .command("claim")
2513
+ .description("Assign a story to yourself and move it to in_progress")
2514
+ .requiredOption("--story <id>", "Story id, e.g. US-001")
2515
+ .option("--force", "Take the story even when someone else holds it")).action((opts) => wrap(() => runClaim(opts))());
2516
+ mesh
2517
+ .command("overlap")
2518
+ .description("List other sessions working on the same story, branch, or files")
2519
+ .requiredOption("--company <slug|uid>", "Company slug or cloud uid")
2520
+ .requiredOption("--session <sid>", "Your session id")
2521
+ .requiredOption("--repo <name>", "Repository name")
2522
+ .requiredOption("--branch <branch>", "Current branch")
2523
+ .option("--project <id>", "Project id")
2524
+ .option("--story <id>", "Story id")
2525
+ .option("--file <path>", "Repo-relative file you are changing (repeatable)", collectRepeatable)
2526
+ .option("--files-json <json>", "JSON array of repo-relative file paths")
2527
+ .option("--json", "Print machine-readable JSON")
2528
+ .action((opts) => wrap(() => runOverlap(opts))());
2529
+ mesh
2530
+ .command("suggest")
2531
+ .description("Rank teammates and agents to pick up a story")
2532
+ .requiredOption("--company <slug|uid>", "Company slug or cloud uid")
2533
+ .requiredOption("--project <id>", "Project id")
2534
+ .requiredOption("--story <id>", "Story id")
2535
+ .option("--json", "Print machine-readable JSON")
2536
+ .action((opts) => wrap(() => runSuggest(opts))());
2537
+ mesh
2538
+ .command("roster")
2539
+ .description("Show each member's daemon and session reporting state")
2540
+ .requiredOption("--company <slug|uid>", "Company slug or cloud uid")
2541
+ .option("--json", "Print machine-readable JSON")
2542
+ .action((opts) => wrap(() => runRoster(opts))());
2120
2543
  const project = mesh
2121
2544
  .command("project")
2122
2545
  .description("Set a project Board view or register its project channel");
@@ -2157,6 +2580,15 @@ export function registerMeshCommand(program, deps = {}) {
2157
2580
  .option("--channel <channelId>", "Bind the project to an existing company or project channel")
2158
2581
  .option("--json", "Print machine-readable JSON")
2159
2582
  .action((projectId, opts) => wrap(() => runProjectRegister(projectId, opts))());
2583
+ project
2584
+ .command("reassign <projectId>")
2585
+ .description("Move project ownership to another person or agent")
2586
+ .requiredOption("--company <slug|uid>", "Company slug or cloud uid")
2587
+ .requiredOption("--to <uid|email|name>", "New owner (uid, email, or directory name)")
2588
+ .option("--include-open-stories", "Also reassign the project's open stories")
2589
+ .option("--note <text>", "Handoff note")
2590
+ .option("--json", "Print machine-readable JSON")
2591
+ .action((projectId, opts) => wrap(() => runProjectReassign(projectId, opts))());
2160
2592
  project
2161
2593
  .command("member")
2162
2594
  .description("Manage project members")
@@ -2217,6 +2649,15 @@ export function registerMeshCommand(program, deps = {}) {
2217
2649
  .requiredOption("--file <path>", "prd.json written by the bound session")
2218
2650
  .option("--json", "Print machine-readable JSON")
2219
2651
  .action((opts) => wrap(() => runPrdSync(opts))());
2652
+ context
2653
+ .command("auto-bind")
2654
+ .description("Bind a needs_project session to the project its repo and branch belong to (local-only, fail-open)")
2655
+ .requiredOption("--session <sid>", "Canonical session id")
2656
+ .option("--cwd <dir>", "Working directory to read the repo and branch from")
2657
+ .option("--company <slug>", "Company slug (defaults to the session's company)")
2658
+ .option("--dry-run", "Print the decision without binding")
2659
+ .option("--json", "Print machine-readable JSON")
2660
+ .action((opts) => wrap(() => runContextAutoBind(opts))());
2220
2661
  context
2221
2662
  .command("organize")
2222
2663
  .description("List or submit one-time project/task decisions (US-007B / US-005B)")
@@ -160,6 +160,19 @@ export function isDefaultOnFastFamily(flagKey) {
160
160
  }
161
161
  /** The relocated scripts that qualify for the entrypoint fast path. */
162
162
  export const FAST_SCAFFOLD_SCRIPTS = [
163
+ {
164
+ name: "work-mesh-project-registration-offer",
165
+ kind: "native",
166
+ modulePath: "./commands/core-bucket-c-workflow-gate-fast.js",
167
+ exportName: "runWorkMeshProjectRegistrationOfferFast",
168
+ flagKey: "hq-core-native-fast-route",
169
+ familyFlagKey: "core-native-bucket-c",
170
+ asset: "core/scripts/work-mesh-project-registration-offer.sh",
171
+ requiresNodeNetworkCompatibility: true,
172
+ root: "live",
173
+ shellCwd: "live",
174
+ shellHqRoot: "live",
175
+ },
163
176
  {
164
177
  name: "handoff-sync-prefetch",
165
178
  kind: "native",
@@ -208,6 +221,19 @@ export const FAST_SCAFFOLD_SCRIPTS = [
208
221
  shellCwd: "live",
209
222
  shellHqRoot: "live",
210
223
  },
224
+ {
225
+ name: "workflow-gate",
226
+ kind: "native",
227
+ modulePath: "./commands/core-bucket-c-workflow-gate-fast.js",
228
+ exportName: "runWorkflowGateFast",
229
+ flagKey: "hq-core-native-fast-route",
230
+ familyFlagKey: "core-native-bucket-c",
231
+ asset: "core/scripts/workflow-gate.sh",
232
+ requiresNodeNetworkCompatibility: true,
233
+ root: "live",
234
+ shellCwd: "live",
235
+ shellHqRoot: "live",
236
+ },
211
237
  {
212
238
  name: "sentry report",
213
239
  kind: "native",
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Tell a lane's senior when a provider 429 put an account on a quota hold.
3
+ *
4
+ * An account-wide hold stops every lane on that account, so the senior needs
5
+ * to see it with the provider's own words and the command that lifts it. One
6
+ * message per (provider, account, hold), so several lanes failing on the same
7
+ * hold produce one notice.
8
+ */
9
+ import type { QuotaState } from "./quota.js";
10
+ import { type LaneProvider, type LaneRecord } from "./types.js";
11
+ export interface QuotaHoldNotice {
12
+ message_id: string;
13
+ text: string;
14
+ }
15
+ /** Build the notice for the account's current hold, or null when there is none. */
16
+ export declare function quotaHoldNotice(state: QuotaState, provider: LaneProvider, accountId: string, laneId: string): QuotaHoldNotice | null;
17
+ /**
18
+ * Deliver the notice to the lane's senior inbox. A session senior gets it in
19
+ * its monitor-session inbox, a lane senior in that lane's dropbox. The notice
20
+ * is always written to stderr as well, so a senior with no inbox still has it
21
+ * in the watcher log. Returns where it was delivered.
22
+ */
23
+ export declare function deliverQuotaHoldNotice(hqRoot: string, lane: Pick<LaneRecord, "lane_id" | "senior">, notice: QuotaHoldNotice, at: string): "session_inbox" | "lane_inbox" | "stderr_only";
24
+ //# sourceMappingURL=quota-notice.d.ts.map