@indigoai-us/hq-cli 5.345.122 → 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.
- package/CHANGELOG.md +25 -0
- package/README.md +15 -0
- package/assets/scaffold/core/scripts/handoff-sync-prefetch.sh +121 -0
- package/assets/scaffold/core/scripts/handoff-sync-publish.sh +124 -0
- package/assets/scaffold/core/scripts/knowledge-pulse-lock.sh +27 -0
- package/assets/scaffold/core/scripts/lib/session-skill-catalog.sh +226 -0
- package/assets/scaffold/core/scripts/skill-installed.sh +43 -0
- package/assets/scaffold/core/scripts/work-mesh-project-registration-offer.sh +77 -0
- package/assets/scaffold/core/scripts/workflow-gate.sh +247 -0
- package/dist/command-catalog.generated.d.ts +332 -0
- package/dist/command-catalog.generated.js +402 -0
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/core-bucket-c-handoff-sync-fast.d.ts +3 -0
- package/dist/commands/core-bucket-c-handoff-sync-fast.js +16 -0
- package/dist/commands/core-bucket-c-handoff-sync-native.d.ts +45 -0
- package/dist/commands/core-bucket-c-handoff-sync-native.js +300 -0
- package/dist/commands/core-bucket-c-small-fast.d.ts +5 -0
- package/dist/commands/core-bucket-c-small-fast.js +19 -0
- package/dist/commands/core-bucket-c-small-native.d.ts +11 -0
- package/dist/commands/core-bucket-c-small-native.js +197 -0
- package/dist/commands/core-bucket-c-work-mesh-offer-native.d.ts +7 -0
- package/dist/commands/core-bucket-c-work-mesh-offer-native.js +147 -0
- package/dist/commands/core-bucket-c-workflow-gate-fast.d.ts +3 -0
- package/dist/commands/core-bucket-c-workflow-gate-fast.js +17 -0
- package/dist/commands/core-bucket-c-workflow-gate-native.d.ts +7 -0
- package/dist/commands/core-bucket-c-workflow-gate-native.js +250 -0
- package/dist/commands/core-derive-trigger-facts-native.js +4 -1
- package/dist/commands/core.js +37 -0
- package/dist/commands/deploy-comments.d.ts +2 -0
- package/dist/commands/deploy-comments.js +2 -2
- package/dist/commands/deploy-list.d.ts +22 -0
- package/dist/commands/deploy-list.js +153 -0
- package/dist/commands/lanes.js +41 -2
- package/dist/commands/mesh.js +457 -16
- package/dist/fast-scaffold.js +74 -0
- package/dist/lib/core-utils/session-company.d.ts +3 -0
- package/dist/lib/core-utils/session-company.js +78 -0
- package/dist/lib/lanes/quota-notice.d.ts +24 -0
- package/dist/lib/lanes/quota-notice.js +61 -0
- package/dist/lib/lanes/quota.d.ts +44 -8
- package/dist/lib/lanes/quota.js +187 -45
- package/dist/lib/lanes/reconcile.js +17 -1
- package/dist/lib/lanes/watcher.js +13 -1
- package/dist/lib/mesh/api.d.ts +2 -0
- package/dist/lib/mesh/api.js +3 -0
- package/dist/lib/mesh/client.d.ts +6 -1
- package/dist/lib/mesh/client.js +18 -5
- package/dist/lib/mesh/team-coordination.d.ts +106 -0
- package/dist/lib/mesh/team-coordination.js +297 -0
- package/dist/lib/work-context/auto-bind.d.ts +75 -0
- package/dist/lib/work-context/auto-bind.js +245 -0
- package/package.json +1 -1
package/dist/commands/mesh.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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({
|
|
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
|
|
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")
|
|
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)")
|
package/dist/fast-scaffold.js
CHANGED
|
@@ -160,6 +160,80 @@ 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
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: "handoff-sync-prefetch",
|
|
178
|
+
kind: "native",
|
|
179
|
+
modulePath: "./commands/core-bucket-c-handoff-sync-fast.js",
|
|
180
|
+
exportName: "runHandoffSyncPrefetchFast",
|
|
181
|
+
flagKey: "hq-core-native-fast-route",
|
|
182
|
+
familyFlagKey: "core-native-bucket-c",
|
|
183
|
+
asset: "core/scripts/handoff-sync-prefetch.sh",
|
|
184
|
+
root: "live",
|
|
185
|
+
shellCwd: "live",
|
|
186
|
+
shellHqRoot: "live",
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: "knowledge-pulse-lock",
|
|
190
|
+
kind: "native",
|
|
191
|
+
modulePath: "./commands/core-bucket-c-small-fast.js",
|
|
192
|
+
exportName: "runKnowledgePulseLockFast",
|
|
193
|
+
flagKey: "hq-core-native-fast-route",
|
|
194
|
+
familyFlagKey: "core-native-bucket-c",
|
|
195
|
+
asset: "core/scripts/knowledge-pulse-lock.sh",
|
|
196
|
+
root: "live",
|
|
197
|
+
shellCwd: "live",
|
|
198
|
+
shellHqRoot: "live",
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: "handoff-sync-publish",
|
|
202
|
+
kind: "native",
|
|
203
|
+
modulePath: "./commands/core-bucket-c-handoff-sync-fast.js",
|
|
204
|
+
exportName: "runHandoffSyncPublishFast",
|
|
205
|
+
flagKey: "hq-core-native-fast-route",
|
|
206
|
+
familyFlagKey: "core-native-bucket-c",
|
|
207
|
+
asset: "core/scripts/handoff-sync-publish.sh",
|
|
208
|
+
root: "live",
|
|
209
|
+
shellCwd: "live",
|
|
210
|
+
shellHqRoot: "live",
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: "skill-installed",
|
|
214
|
+
kind: "native",
|
|
215
|
+
modulePath: "./commands/core-bucket-c-small-fast.js",
|
|
216
|
+
exportName: "runSkillInstalledFast",
|
|
217
|
+
flagKey: "hq-core-native-fast-route",
|
|
218
|
+
familyFlagKey: "core-native-bucket-c",
|
|
219
|
+
asset: "core/scripts/skill-installed.sh",
|
|
220
|
+
root: "live",
|
|
221
|
+
shellCwd: "live",
|
|
222
|
+
shellHqRoot: "live",
|
|
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
|
+
},
|
|
163
237
|
{
|
|
164
238
|
name: "sentry report",
|
|
165
239
|
kind: "native",
|