@indigoai-us/hq-cli 5.345.32 → 5.345.34
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 +15 -0
- package/dist/command-catalog.generated.d.ts +92 -3
- package/dist/command-catalog.generated.js +111 -3
- package/dist/commands/cloud-provision.d.ts +2 -0
- package/dist/commands/db-dump.d.ts +8 -0
- package/dist/commands/db-dump.js +50 -0
- package/dist/commands/db-provision.d.ts +3 -10
- package/dist/commands/db-provision.js +26 -47
- package/dist/commands/db-remote-deps.d.ts +18 -0
- package/dist/commands/db-remote-deps.js +59 -0
- package/dist/commands/db-sql.d.ts +2 -1
- package/dist/commands/db-sql.js +30 -5
- package/dist/commands/db-usage.d.ts +7 -0
- package/dist/commands/db-usage.js +28 -0
- package/dist/commands/db.js +4 -0
- package/dist/commands/files.js +33 -5
- package/dist/commands/lanes-message.js +40 -8
- package/dist/commands/lanes-questions.d.ts +6 -1
- package/dist/commands/lanes-questions.js +19 -7
- package/dist/commands/lanes-resume.js +17 -6
- package/dist/commands/lanes-shared.d.ts +6 -1
- package/dist/commands/lanes.js +92 -15
- package/dist/commands/mesh.js +150 -26
- package/dist/commands/reindex.js +11 -8
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +68 -604
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/daemon/remote-control.js +62 -3
- package/dist/lib/db/control-plane.d.ts +32 -0
- package/dist/lib/db/control-plane.js +50 -0
- package/dist/lib/db/redact.d.ts +4 -0
- package/dist/lib/db/redact.js +17 -0
- package/dist/lib/db/remote-engine.d.ts +67 -0
- package/dist/lib/db/remote-engine.js +165 -0
- package/dist/lib/db/usage-format.d.ts +4 -0
- package/dist/lib/db/usage-format.js +41 -0
- package/dist/lib/index-render/shared.d.ts +3 -1
- package/dist/lib/index-render/shared.js +5 -2
- package/dist/lib/lanes/depth-cap.d.ts +7 -0
- package/dist/lib/lanes/depth-cap.js +41 -0
- package/dist/lib/lanes/keep-alive.d.ts +12 -0
- package/dist/lib/lanes/keep-alive.js +25 -6
- package/dist/lib/lanes/reconcile.d.ts +1 -1
- package/dist/lib/lanes/reconcile.js +16 -5
- package/dist/lib/mesh/api.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/install.d.ts +7 -4
- package/dist/lib/mesh/live/daemon/install.js +144 -47
- package/dist/lib/mesh/project-board.d.ts +17 -0
- package/dist/lib/mesh/project-board.js +28 -0
- package/dist/lib/mesh/story-status.d.ts +16 -0
- package/dist/lib/mesh/story-status.js +32 -7
- package/dist/utils/self-update.d.ts +2 -0
- package/dist/utils/self-update.js +61 -23
- package/dist/utils/version-check.d.ts +1 -1
- package/dist/utils/version-check.js +2 -2
- package/dist/utils/version-gate.d.ts +21 -14
- package/dist/utils/version-gate.js +119 -28
- package/package.json +4 -1
package/dist/commands/lanes.js
CHANGED
|
@@ -33,6 +33,7 @@ import { registerLaneReviewCommand } from "./lanes-review.js";
|
|
|
33
33
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
34
34
|
import { withCliFeatureUsed } from "../utils/cli-telemetry.js";
|
|
35
35
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
36
|
+
import { laneDepthCap } from "../lib/lanes/depth-cap.js";
|
|
36
37
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
37
38
|
import { runResidentCommandWithPackageLease } from "../utils/resident-package-preload.js";
|
|
38
39
|
import { readLanesFlagLocalOverride, readLanesRegistryFlag, readLanesRegistryFlagFailOff } from "../lib/lanes/registry-flag.js";
|
|
@@ -66,7 +67,7 @@ import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STA
|
|
|
66
67
|
import { laneStories, storyWarning, } from "../lib/lanes/stories.js";
|
|
67
68
|
import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
|
|
68
69
|
import { laneCompanyUidSource, resolveLaneCompany } from "../lib/lanes/lane-company.js";
|
|
69
|
-
import { doneKeepAliveVerdict, hasPendingKeepAliveWork, keepAliveNextCheckAtMs, keepAliveExclusion, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
|
|
70
|
+
import { doneKeepAliveVerdict, hasPendingKeepAliveWork, keepAliveNextCheckAtMs, keepAliveExclusion, keepAliveSupersededBy, loadRetiredLaneEntries, matchesRetiredLaneEntry, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
|
|
70
71
|
import { questionBodyAbsPath, resolveQuestionCompanyWithFallback, resolveProjectQuestionCompany, } from "../lib/lanes/questions.js";
|
|
71
72
|
import { assertQuestionBodySecretSafe } from "../lib/lanes/questions.js";
|
|
72
73
|
import { writeQuestionBody } from "../lib/lanes/questions.js";
|
|
@@ -2070,12 +2071,12 @@ function seniorLaneChain(hqRoot, ref, ctx, visited = new Set(), override) {
|
|
|
2070
2071
|
const depth = isOverridden ? override.depth : derivedLaneDepth(hqRoot, lane.senior, ctx, override);
|
|
2071
2072
|
return [...prefix, { lane_id: lane.lane_id, depth }];
|
|
2072
2073
|
}
|
|
2073
|
-
function maxLaneDepthError(depth, chain, description, extraDetails = {}) {
|
|
2074
|
-
const parentLane = [...chain].reverse().find((entry) => entry.depth <
|
|
2074
|
+
function maxLaneDepthError(depth, chain, description, cap, extraDetails = {}) {
|
|
2075
|
+
const parentLane = [...chain].reverse().find((entry) => entry.depth < cap.max);
|
|
2075
2076
|
const nextStep = parentLane
|
|
2076
2077
|
? `Ask your senior lane ${parentLane.lane_id} to create this worker instead, or re-parent this lane under a shallower senior.`
|
|
2077
2078
|
: "Ask a shallower senior to create this lane, or re-parent it to a shallower senior.";
|
|
2078
|
-
return new LanesError("max_lane_depth", `${description}: senior chain length ${depth} exceeds cap ${
|
|
2079
|
+
return new LanesError("max_lane_depth", `${description}: senior chain length ${depth} exceeds cap ${cap.max} (${cap.source === "company" ? "company config" : "default config"})`, { ...extraDetails, depth, max: cap.max, source: cap.source, senior_chain: chain, next_step: nextStep });
|
|
2079
2080
|
}
|
|
2080
2081
|
function callerDepth(hqRoot, ctx) {
|
|
2081
2082
|
const laneId = ctx.env.HQ_LANE_ID?.trim();
|
|
@@ -3068,21 +3069,22 @@ async function runCreate(ctx, opts) {
|
|
|
3068
3069
|
}
|
|
3069
3070
|
assertKnownOutpost(requested, known);
|
|
3070
3071
|
}
|
|
3072
|
+
let company = companyFlag
|
|
3073
|
+
? await ctx.resolveCompany(companyFlag)
|
|
3074
|
+
: null;
|
|
3075
|
+
const depthCap = laneDepthCap(hqRoot, company?.slug);
|
|
3071
3076
|
const depth = callerDepth(hqRoot, ctx);
|
|
3072
|
-
if (depth >=
|
|
3077
|
+
if (depth >= depthCap.max) {
|
|
3073
3078
|
const callerLaneId = ctx.env.HQ_LANE_ID?.trim();
|
|
3074
3079
|
const chain = callerLaneId
|
|
3075
3080
|
? seniorLaneChain(hqRoot, { kind: "lane", id: callerLaneId }, ctx)
|
|
3076
3081
|
: seniorLaneChain(hqRoot, sessionIdFromEnv(ctx.env)
|
|
3077
3082
|
? { kind: "session", id: sessionIdFromEnv(ctx.env) }
|
|
3078
3083
|
: { kind: "user" }, ctx);
|
|
3079
|
-
throw maxLaneDepthError(depth + 1, chain, `A depth-${depth} caller may not spawn a depth-${depth + 1} lane
|
|
3084
|
+
throw maxLaneDepthError(depth + 1, chain, `A depth-${depth} caller may not spawn a depth-${depth + 1} lane`, depthCap);
|
|
3080
3085
|
}
|
|
3081
3086
|
const senior = opts.senior ? parseSeniorRef(opts.senior) : defaultSenior(ctx);
|
|
3082
3087
|
assertSeniorExists(hqRoot, senior, ctx);
|
|
3083
|
-
let company = companyFlag
|
|
3084
|
-
? await ctx.resolveCompany(companyFlag)
|
|
3085
|
-
: null;
|
|
3086
3088
|
const seniorCompany = companyOfSenior(hqRoot, senior, ctx);
|
|
3087
3089
|
if (company === null && seniorCompany.binding === "company") {
|
|
3088
3090
|
const seniorCompanyIdentifier = seniorCompany.slug ?? (seniorCompany.uid
|
|
@@ -3104,8 +3106,8 @@ async function runCreate(ctx, opts) {
|
|
|
3104
3106
|
}
|
|
3105
3107
|
const hireGrant = requestedHireGrant ?? 0;
|
|
3106
3108
|
const laneDepth = derivedLaneDepth(hqRoot, senior, ctx);
|
|
3107
|
-
if (laneDepth >
|
|
3108
|
-
throw maxLaneDepthError(laneDepth, seniorLaneChain(hqRoot, senior, ctx), `Creating lane at depth ${laneDepth} is refused
|
|
3109
|
+
if (laneDepth > depthCap.max) {
|
|
3110
|
+
throw maxLaneDepthError(laneDepth, seniorLaneChain(hqRoot, senior, ctx), `Creating lane at depth ${laneDepth} is refused`, depthCap);
|
|
3109
3111
|
}
|
|
3110
3112
|
// A no-company lane must not call company resolution (openMeshTransport).
|
|
3111
3113
|
if (storyIds.length > 0 && ctx.storyLedgerEnabled()) {
|
|
@@ -6007,6 +6009,71 @@ async function runKeepAlive(ctx, laneId, opts) {
|
|
|
6007
6009
|
lane: updated,
|
|
6008
6010
|
});
|
|
6009
6011
|
}
|
|
6012
|
+
async function runLaneDoctor(ctx, pendingQuestionsForLane) {
|
|
6013
|
+
const hqRoot = ctx.hqRoot();
|
|
6014
|
+
const lanes = loadAllLanes(hqRoot);
|
|
6015
|
+
const retiredEntries = loadRetiredLaneEntries(hqRoot);
|
|
6016
|
+
const findings = [];
|
|
6017
|
+
for (const lane of lanes) {
|
|
6018
|
+
if (lane.keep_alive !== true || isTerminalLaneState(lane.state))
|
|
6019
|
+
continue;
|
|
6020
|
+
const retiredEntry = retiredEntries.find((entry) => matchesRetiredLaneEntry(entry, lane.lane_id));
|
|
6021
|
+
if (retiredEntry !== undefined) {
|
|
6022
|
+
findings.push({
|
|
6023
|
+
kind: "retired_keep_alive_conflict",
|
|
6024
|
+
lane_id: lane.lane_id,
|
|
6025
|
+
state: lane.state,
|
|
6026
|
+
registry_entry: {
|
|
6027
|
+
path: path.join(hqRoot, "workspace", "lane-briefs", "juniors.json"),
|
|
6028
|
+
index: retiredEntry.index,
|
|
6029
|
+
lane: retiredEntry.lane,
|
|
6030
|
+
retired_at: retiredEntry.retired_at ?? null,
|
|
6031
|
+
},
|
|
6032
|
+
});
|
|
6033
|
+
}
|
|
6034
|
+
const companyForLane = (candidate) => resolveLaneCompany(hqRoot, candidate).company;
|
|
6035
|
+
const company = companyForLane(lane);
|
|
6036
|
+
const lineageAware = await readLanesRegistryFlag(LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, {
|
|
6037
|
+
company: company ?? undefined,
|
|
6038
|
+
env: ctx.env,
|
|
6039
|
+
});
|
|
6040
|
+
const successor = keepAliveSupersededBy(lane, lanes, lineageAware, companyForLane);
|
|
6041
|
+
if (successor !== undefined) {
|
|
6042
|
+
findings.push({
|
|
6043
|
+
kind: "superseded_keep_alive_conflict",
|
|
6044
|
+
lane_id: lane.lane_id,
|
|
6045
|
+
state: lane.state,
|
|
6046
|
+
superseded_by: successor.lane_id,
|
|
6047
|
+
suggested_action: `Review successor ${successor.lane_id} and correct stale supersession metadata before retrying. Message delivery still requires an authorized caller and the running-superseded delivery flag.`,
|
|
6048
|
+
});
|
|
6049
|
+
}
|
|
6050
|
+
if (lane.state === "awaiting_input") {
|
|
6051
|
+
const staleCutoff = Date.parse(ctx.now().toISOString()) - 24 * 60 * 60 * 1000;
|
|
6052
|
+
const pendingQuestions = await pendingQuestionsForLane(ctx, lane);
|
|
6053
|
+
for (const question of pendingQuestions) {
|
|
6054
|
+
const askedAt = Date.parse(question.asked_at);
|
|
6055
|
+
if (!Number.isFinite(askedAt) || askedAt > staleCutoff)
|
|
6056
|
+
continue;
|
|
6057
|
+
const companySlug = lane.company?.slug ?? lane.mesh_company ?? "<company-slug>";
|
|
6058
|
+
findings.push({
|
|
6059
|
+
kind: "awaiting_input_keep_alive",
|
|
6060
|
+
lane_id: lane.lane_id,
|
|
6061
|
+
state: lane.state,
|
|
6062
|
+
question_id: question.question_id,
|
|
6063
|
+
asked_at: question.asked_at,
|
|
6064
|
+
audience: question.audience,
|
|
6065
|
+
suggested_command: `hq lanes questions answer ${question.question_id} --company ${companySlug} --freeform <answer>`,
|
|
6066
|
+
});
|
|
6067
|
+
}
|
|
6068
|
+
}
|
|
6069
|
+
}
|
|
6070
|
+
printJson({
|
|
6071
|
+
ok: findings.length === 0,
|
|
6072
|
+
action: "doctor",
|
|
6073
|
+
checked_keep_alive_lanes: lanes.filter((lane) => lane.keep_alive === true && !isTerminalLaneState(lane.state)).length,
|
|
6074
|
+
findings,
|
|
6075
|
+
});
|
|
6076
|
+
}
|
|
6010
6077
|
function assertKeepAliveChangeAllowed(ctx, lane, laneId) {
|
|
6011
6078
|
const callerAuthorization = callerSeniorAuthorization(ctx, lane.senior);
|
|
6012
6079
|
if (!callerAuthorization.authorized) {
|
|
@@ -7184,10 +7251,11 @@ function runReparentLocked(ctx, laneId, opts, finishedChildrenDoNotCount = false
|
|
|
7184
7251
|
throw new LanesError("senior_cycle", `Re-parent would create a cycle through ${laneId}`, { lane_id: laneId, senior: nextSenior });
|
|
7185
7252
|
}
|
|
7186
7253
|
const newDepth = derivedLaneDepth(hqRoot, nextSenior, ctx);
|
|
7187
|
-
|
|
7254
|
+
const depthCap = laneDepthCap(hqRoot, lane.company?.slug);
|
|
7255
|
+
if (newDepth > depthCap.max) {
|
|
7188
7256
|
const chain = seniorLaneChain(hqRoot, nextSenior, ctx);
|
|
7189
7257
|
chain.push({ lane_id: lane.lane_id, depth: newDepth });
|
|
7190
|
-
throw maxLaneDepthError(newDepth, chain, `Re-parent would put lane at depth ${newDepth}
|
|
7258
|
+
throw maxLaneDepthError(newDepth, chain, `Re-parent would put lane at depth ${newDepth}`, depthCap);
|
|
7191
7259
|
}
|
|
7192
7260
|
const pending = pendingQuestionsForLane(hqRoot, laneId);
|
|
7193
7261
|
const oldDepth = derivedLaneDepth(hqRoot, lane.senior, ctx);
|
|
@@ -7204,10 +7272,11 @@ function runReparentLocked(ctx, laneId, opts, finishedChildrenDoNotCount = false
|
|
|
7204
7272
|
assertLaneRunsHereForOperatorMutation(ctx, other, "reparent descendant of");
|
|
7205
7273
|
const oldDescendantDepth = derivedLaneDepth(hqRoot, other.senior, ctx);
|
|
7206
7274
|
const descendantDepth = derivedLaneDepth(hqRoot, other.senior, ctx, override);
|
|
7207
|
-
|
|
7275
|
+
const descendantCap = laneDepthCap(hqRoot, other.company?.slug);
|
|
7276
|
+
if (descendantDepth > descendantCap.max) {
|
|
7208
7277
|
const chain = seniorLaneChain(hqRoot, other.senior, ctx, new Set(), override);
|
|
7209
7278
|
chain.push({ lane_id: other.lane_id, depth: descendantDepth });
|
|
7210
|
-
throw maxLaneDepthError(descendantDepth, chain, `Re-parent would put descendant ${other.lane_id} at depth ${descendantDepth}`, { lane_id: other.lane_id });
|
|
7279
|
+
throw maxLaneDepthError(descendantDepth, chain, `Re-parent would put descendant ${other.lane_id} at depth ${descendantDepth}`, descendantCap, { lane_id: other.lane_id });
|
|
7211
7280
|
}
|
|
7212
7281
|
if (descendantDepth > oldDescendantDepth &&
|
|
7213
7282
|
pendingQuestionsForLane(hqRoot, other.lane_id).length > 0) {
|
|
@@ -8646,6 +8715,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8646
8715
|
resolveCommandLane: resolveMessageLaneTarget,
|
|
8647
8716
|
resolveActiveQuestionCompany,
|
|
8648
8717
|
hasPendingQuestionForLane: questionHandlers.hasPendingQuestionForLane,
|
|
8718
|
+
pendingQuestionsForLane: questionHandlers.pendingQuestionsForLane,
|
|
8649
8719
|
cancelFallbackQuestionsForLane: questionHandlers.cancelFallbackQuestionsForLane,
|
|
8650
8720
|
resolveResumeProfile,
|
|
8651
8721
|
reuseProfileRefreshEnabled,
|
|
@@ -8677,6 +8747,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8677
8747
|
recordResumeSetup,
|
|
8678
8748
|
resolveCommandLane: (commandCtx, laneId) => resolveCommandLaneTarget(commandCtx, laneId),
|
|
8679
8749
|
hasPendingQuestionForLane: questionHandlers.hasPendingQuestionForLane,
|
|
8750
|
+
pendingQuestionsForLane: questionHandlers.pendingQuestionsForLane,
|
|
8680
8751
|
cancelFallbackQuestionsForLane: questionHandlers.cancelFallbackQuestionsForLane,
|
|
8681
8752
|
resolveResumeProfile,
|
|
8682
8753
|
reuseProfileRefreshEnabled,
|
|
@@ -8862,6 +8933,12 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8862
8933
|
.action(wrap(async (opts) => {
|
|
8863
8934
|
await runBackfillGrants(ctx, opts);
|
|
8864
8935
|
}));
|
|
8936
|
+
lanes
|
|
8937
|
+
.command("doctor")
|
|
8938
|
+
.description("Report active keep-alive lanes blocked by retired, superseded, or pending-question state")
|
|
8939
|
+
.action(wrap(async () => {
|
|
8940
|
+
await runLaneDoctor(ctx, questionHandlers.pendingQuestionsForLane);
|
|
8941
|
+
}));
|
|
8865
8942
|
lanes
|
|
8866
8943
|
.command("list")
|
|
8867
8944
|
.description("List lanes")
|
package/dist/commands/mesh.js
CHANGED
|
@@ -14,8 +14,8 @@ 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
16
|
import { STORY_STATUSES, appendThreadEvent, addProjectMember, ensureProjectThread, eventPayload, getProjectView, listActiveMembershipCompanies, listActiveThreads, patchStory, putProjectView, registerMeshProject, resolveActiveMembershipCompany, resolveMeshPrincipalUid, warmMeshConversationCache, } from "../lib/mesh/api.js";
|
|
17
|
-
import { closeSupersededProjectThread, ensureMissingBoardStories, ensureProjectBoard, boardStoryStatus, formatStoryStatusChanges, mergeProjectStories, prdSyncEventsThatChangeBoard, } from "../lib/mesh/project-board.js";
|
|
18
|
-
import { mapSourceStoryStatus } from "../lib/mesh/story-status.js";
|
|
17
|
+
import { closeSupersededProjectThread, ensureMissingBoardStories, ensureProjectBoard, boardStoryStatus, formatStoryStatusChanges, mergeProjectStories, prdSyncEventsThatChangeBoard, reconcileProjectStories, } from "../lib/mesh/project-board.js";
|
|
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
20
|
import { createCandidatesFetcher, createMigratePoster, createOrganizePoster, createWorkSessionDeliverer, fetchCompanyLive, formatCompanyLiveTable, openMeshTransport, probeMigrationCapabilityForMemberships, requireToken, } from "../lib/mesh/client.js";
|
|
21
21
|
import { clearDefaultCompany, effectiveUsageAttribution, isDefaultCompanyDisabled, readDeviceConfig, recordMigrationCapabilitySnapshot, setRepairHeldWithDefault, setDefaultCompany, setUsageAttribution, } from "../lib/work-context/config.js";
|
|
@@ -257,32 +257,43 @@ function projectStoriesFromJson(value, source) {
|
|
|
257
257
|
if (!rawStories) {
|
|
258
258
|
fail(`${source} must be a JSON story array or an object with userStories.`);
|
|
259
259
|
}
|
|
260
|
-
const
|
|
260
|
+
const rejectedStories = [];
|
|
261
|
+
const stories = rawStories.flatMap((raw, index) => {
|
|
261
262
|
const story = record(raw);
|
|
262
263
|
const id = typeof story?.id === "string" ? story.id.trim() : "";
|
|
263
264
|
const title = typeof story?.title === "string" ? story.title.trim() : "";
|
|
264
265
|
if (!story || !id || !title) {
|
|
265
266
|
fail(`${source} userStories[${index}] requires string id and title.`);
|
|
266
267
|
}
|
|
267
|
-
|
|
268
|
+
let status;
|
|
269
|
+
try {
|
|
270
|
+
status = mapSourceStoryStatus({ ...story, id }, `${source} userStories[${index}]`);
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
if (!(err instanceof UnknownProjectStoryStateError))
|
|
274
|
+
throw err;
|
|
275
|
+
rejectedStories.push(err);
|
|
276
|
+
return [];
|
|
277
|
+
}
|
|
268
278
|
const resolvedStatus = status || (story.passes === true ? "done" : undefined);
|
|
269
|
-
return {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
279
|
+
return [{
|
|
280
|
+
id,
|
|
281
|
+
title,
|
|
282
|
+
...(typeof story.description === "string" ? { description: story.description } : {}),
|
|
283
|
+
...(Array.isArray(story.acceptanceCriteria)
|
|
284
|
+
? {
|
|
285
|
+
acceptanceCriteria: story.acceptanceCriteria.filter((item) => typeof item === "string"),
|
|
286
|
+
}
|
|
287
|
+
: {}),
|
|
288
|
+
...(resolvedStatus ? { status: resolvedStatus } : {}),
|
|
289
|
+
...(typeof story.state === "string" ? { ledgerState: story.state.trim() } : {}),
|
|
290
|
+
// hq-pro makes passes mirror the Board status. A completed PRD story
|
|
291
|
+
// without an explicit status must therefore become done.
|
|
292
|
+
passes: resolvedStatus === "done",
|
|
293
|
+
...(typeof story.priority === "number" && Number.isFinite(story.priority)
|
|
294
|
+
? { priority: story.priority }
|
|
295
|
+
: {}),
|
|
296
|
+
}];
|
|
286
297
|
});
|
|
287
298
|
const metadata = record(plan?.metadata);
|
|
288
299
|
const repos = metadata && Object.hasOwn(metadata, "repos")
|
|
@@ -292,12 +303,44 @@ function projectStoriesFromJson(value, source) {
|
|
|
292
303
|
...(typeof plan?.name === "string" ? { name: plan.name } : {}),
|
|
293
304
|
...(typeof plan?.description === "string" ? { description: plan.description } : {}),
|
|
294
305
|
stories,
|
|
306
|
+
rejectedStories,
|
|
295
307
|
...(repos ? { repos } : {}),
|
|
296
308
|
};
|
|
297
309
|
}
|
|
298
|
-
function
|
|
310
|
+
function projectStateErrors(rejected) {
|
|
311
|
+
return rejected.map((error) => ({
|
|
312
|
+
code: error.code,
|
|
313
|
+
file: error.source.split(" userStories[")[0],
|
|
314
|
+
storyId: error.storyId,
|
|
315
|
+
field: "state",
|
|
316
|
+
value: error.value,
|
|
317
|
+
accepted: ACCEPTED_LEDGER_STATES,
|
|
318
|
+
}));
|
|
319
|
+
}
|
|
320
|
+
class RejectedProjectStoryStatesError extends Error {
|
|
321
|
+
rejected;
|
|
322
|
+
code = "PROJECT_STORY_STATE_INVALID";
|
|
323
|
+
constructor(rejected) {
|
|
324
|
+
super(rejected.map((error) => `${error.code}: ${error.message}`).join("\n"));
|
|
325
|
+
this.rejected = rejected;
|
|
326
|
+
this.name = "RejectedProjectStoryStatesError";
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function failProjectStoryStates(rejected, json) {
|
|
330
|
+
if (json)
|
|
331
|
+
console.log(JSON.stringify({ ok: false, errors: projectStateErrors(rejected) }));
|
|
332
|
+
else
|
|
333
|
+
for (const error of rejected)
|
|
334
|
+
console.error(`${error.code}: ${error.message}`);
|
|
335
|
+
process.exitCode = 1;
|
|
336
|
+
}
|
|
337
|
+
function parseProjectPlan(raw, source, options = {}) {
|
|
299
338
|
try {
|
|
300
|
-
|
|
339
|
+
const plan = projectStoriesFromJson(JSON.parse(raw), source);
|
|
340
|
+
if (plan.rejectedStories.length && !options.allowRejectedStories) {
|
|
341
|
+
throw new RejectedProjectStoryStatesError(plan.rejectedStories);
|
|
342
|
+
}
|
|
343
|
+
return plan;
|
|
301
344
|
}
|
|
302
345
|
catch (err) {
|
|
303
346
|
if (err instanceof SyntaxError)
|
|
@@ -330,6 +373,10 @@ async function runProjectSet(projectId, opts) {
|
|
|
330
373
|
: opts.storiesJson
|
|
331
374
|
? parseProjectPlan(opts.storiesJson, "--stories-json")
|
|
332
375
|
: undefined;
|
|
376
|
+
if (source?.rejectedStories.length) {
|
|
377
|
+
failProjectStoryStates(source.rejectedStories, Boolean(opts.json));
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
333
380
|
const explicitRepos = opts.reposJson
|
|
334
381
|
? parseProjectReposJson(opts.reposJson, "--repos-json")
|
|
335
382
|
: opts.repo?.length
|
|
@@ -479,8 +526,12 @@ async function runProjectMemberAdd(projectId, personUid, opts) {
|
|
|
479
526
|
async function runProjectEnsure(projectId, opts) {
|
|
480
527
|
if (!opts.storiesFile?.trim())
|
|
481
528
|
fail("`--stories-file <prd.json>` is required.");
|
|
482
|
-
const source = parseProjectPlan(readFileSync(opts.storiesFile, "utf8"), opts.storiesFile);
|
|
529
|
+
const source = parseProjectPlan(readFileSync(opts.storiesFile, "utf8"), opts.storiesFile, { allowRejectedStories: true });
|
|
483
530
|
const { token, company } = await withCompany(opts);
|
|
531
|
+
if (source.rejectedStories.length && source.stories.length === 0) {
|
|
532
|
+
failProjectStoryStates(source.rejectedStories, Boolean(opts.json));
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
484
535
|
let current;
|
|
485
536
|
try {
|
|
486
537
|
current = await getProjectView(token, company.companyUid, projectId);
|
|
@@ -506,12 +557,13 @@ async function runProjectEnsure(projectId, opts) {
|
|
|
506
557
|
}));
|
|
507
558
|
if (opts.json) {
|
|
508
559
|
console.log(JSON.stringify({
|
|
509
|
-
ok:
|
|
560
|
+
ok: source.rejectedStories.length === 0,
|
|
510
561
|
action: "project.ensure.dry-run",
|
|
511
562
|
projectId,
|
|
512
563
|
companyUid: company.companyUid,
|
|
513
564
|
sourceStatuses,
|
|
514
565
|
stories: boardStatuses,
|
|
566
|
+
...(source.rejectedStories.length ? { errors: projectStateErrors(source.rejectedStories) } : {}),
|
|
515
567
|
}, null, 2));
|
|
516
568
|
}
|
|
517
569
|
else {
|
|
@@ -523,6 +575,8 @@ async function runProjectEnsure(projectId, opts) {
|
|
|
523
575
|
for (const story of boardStatuses)
|
|
524
576
|
console.log(`${story.id}\t${story.status}`);
|
|
525
577
|
}
|
|
578
|
+
if (source.rejectedStories.length)
|
|
579
|
+
process.exitCode = 1;
|
|
526
580
|
return;
|
|
527
581
|
}
|
|
528
582
|
// Genesis thread, then the view, then register. Register returns 403
|
|
@@ -606,13 +660,74 @@ async function runProjectEnsure(projectId, opts) {
|
|
|
606
660
|
channelId: registration.channelId,
|
|
607
661
|
version: verified.version,
|
|
608
662
|
stories: verified.stories,
|
|
663
|
+
...(source.rejectedStories.length ? { ok: false, errors: projectStateErrors(source.rejectedStories) } : {}),
|
|
609
664
|
};
|
|
610
665
|
noteCallingSessionBound(payload.projectId, company);
|
|
611
666
|
if (opts.json) {
|
|
612
667
|
console.log(JSON.stringify(payload));
|
|
668
|
+
if (source.rejectedStories.length)
|
|
669
|
+
process.exitCode = 1;
|
|
613
670
|
return;
|
|
614
671
|
}
|
|
615
672
|
console.log(`Work mesh: project ${payload.projectId} ensured (thread ${payload.threadId}, channel ${payload.channelId}, ${payload.stories.length} stories)`);
|
|
673
|
+
if (source.rejectedStories.length)
|
|
674
|
+
failProjectStoryStates(source.rejectedStories, false);
|
|
675
|
+
}
|
|
676
|
+
async function runProjectReconcile(projectId, opts) {
|
|
677
|
+
if (!opts.storiesFile?.trim())
|
|
678
|
+
fail("`--stories-file <prd.json>` is required.");
|
|
679
|
+
const source = parseProjectPlan(readFileSync(opts.storiesFile, "utf8"), opts.storiesFile, { allowRejectedStories: true });
|
|
680
|
+
const { token, company } = await withCompany(opts);
|
|
681
|
+
let current;
|
|
682
|
+
try {
|
|
683
|
+
current = await getProjectView(token, company.companyUid, projectId);
|
|
684
|
+
}
|
|
685
|
+
catch (err) {
|
|
686
|
+
if (httpStatus(err) !== 404)
|
|
687
|
+
throw err;
|
|
688
|
+
}
|
|
689
|
+
if (current && (current.companyUid !== company.companyUid || current.projectId !== projectId)) {
|
|
690
|
+
fail("Work mesh project reconcile: Board company or project id did not match the request.");
|
|
691
|
+
}
|
|
692
|
+
const reconciliation = reconcileProjectStories(source.stories, current?.stories ?? []);
|
|
693
|
+
const errors = [
|
|
694
|
+
...projectStateErrors(source.rejectedStories),
|
|
695
|
+
...(!current ? [{ code: "PROJECT_BOARD_NOT_FOUND", projectId }] : []),
|
|
696
|
+
];
|
|
697
|
+
const mismatches = reconciliation.mismatches;
|
|
698
|
+
const clean = mismatches.length === 0 && errors.length === 0;
|
|
699
|
+
if (opts.json) {
|
|
700
|
+
console.log(JSON.stringify({
|
|
701
|
+
ok: clean,
|
|
702
|
+
action: "project.reconcile",
|
|
703
|
+
projectId,
|
|
704
|
+
companyUid: company.companyUid,
|
|
705
|
+
mismatches,
|
|
706
|
+
stateDriftCount: reconciliation.stateDriftCount,
|
|
707
|
+
ledgerStateUnsupported: reconciliation.ledgerStateUnsupported,
|
|
708
|
+
errors,
|
|
709
|
+
}, null, 2));
|
|
710
|
+
}
|
|
711
|
+
else if (clean) {
|
|
712
|
+
const note = reconciliation.ledgerStateUnsupported.length
|
|
713
|
+
? `; Board does not return ledgerState for ${reconciliation.ledgerStateUnsupported.length} stories yet`
|
|
714
|
+
: "";
|
|
715
|
+
console.log(`Work mesh: project ${projectId} matches ${source.stories.length} ledger stories${note}`);
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
for (const mismatch of mismatches) {
|
|
719
|
+
console.log(`${mismatch.storyId}\tledger=${mismatch.ledgerState ?? "<none>"}\texpected=${mismatch.expectedBoardStatus ?? "<none>"}\tboard=${mismatch.boardStatus ?? "<missing>"}\tboardLedger=${mismatch.boardLedgerState ?? "<unsupported>"}\tstateDrift=${mismatch.stateDrift}\tmissing=${mismatch.missingOnBoard}\textra=${mismatch.extraOnBoard}`);
|
|
720
|
+
}
|
|
721
|
+
if (reconciliation.ledgerStateUnsupported.length) {
|
|
722
|
+
console.log(`Board does not return ledgerState for ${reconciliation.ledgerStateUnsupported.length} stories: ${reconciliation.ledgerStateUnsupported.join(", ")}`);
|
|
723
|
+
}
|
|
724
|
+
if (!current)
|
|
725
|
+
console.error(`PROJECT_BOARD_NOT_FOUND: Board project ${projectId} was not found.`);
|
|
726
|
+
for (const error of source.rejectedStories)
|
|
727
|
+
console.error(`${error.code}: ${error.message}`);
|
|
728
|
+
}
|
|
729
|
+
if (!clean)
|
|
730
|
+
process.exitCode = 1;
|
|
616
731
|
}
|
|
617
732
|
async function runPrdSync(opts) {
|
|
618
733
|
const sessionId = opts.session.trim();
|
|
@@ -689,6 +804,8 @@ async function runPrdSync(opts) {
|
|
|
689
804
|
});
|
|
690
805
|
}
|
|
691
806
|
catch (err) {
|
|
807
|
+
if (err instanceof RejectedProjectStoryStatesError)
|
|
808
|
+
throw err;
|
|
692
809
|
const reason = err instanceof Error ? err.message : String(err);
|
|
693
810
|
console.error(`work-mesh: prd-sync ensure failed; emitting anyway (${reason.slice(0, 160)})`);
|
|
694
811
|
}
|
|
@@ -828,8 +945,8 @@ async function runBindProject(target, opts) {
|
|
|
828
945
|
}
|
|
829
946
|
if (!current.companyUid)
|
|
830
947
|
fail(`Session ${sessionId} has no company. Bind a company first.`);
|
|
831
|
-
const { token, company } = await withCompany({ company: opts.company ?? current.companySlug });
|
|
832
948
|
const plan = parseProjectPlan(readFileSync(prdPath, "utf8"), prdPath);
|
|
949
|
+
const { token, company } = await withCompany({ company: opts.company ?? current.companySlug });
|
|
833
950
|
await ensureProjectBoard({
|
|
834
951
|
token,
|
|
835
952
|
company: {
|
|
@@ -2026,6 +2143,13 @@ export function registerMeshCommand(program, deps = {}) {
|
|
|
2026
2143
|
.option("--dry-run", "Read the current Board and print computed story statuses without writing")
|
|
2027
2144
|
.option("--json", "Print {projectId, threadId, channelId, version, stories}")
|
|
2028
2145
|
.action((projectId, opts) => wrap(() => runProjectEnsure(projectId, opts))());
|
|
2146
|
+
project
|
|
2147
|
+
.command("reconcile <projectId>")
|
|
2148
|
+
.description("Read the project Board and report drift from a PRD ledger")
|
|
2149
|
+
.requiredOption("--company <slug|uid>", "Company slug or cloud uid")
|
|
2150
|
+
.requiredOption("--stories-file <path>", "PRD JSON file with userStories")
|
|
2151
|
+
.option("--json", "Print machine-readable mismatch records")
|
|
2152
|
+
.action((projectId, opts) => wrap(() => runProjectReconcile(projectId, opts))());
|
|
2029
2153
|
project
|
|
2030
2154
|
.command("register <projectId>")
|
|
2031
2155
|
.description("Register an existing Board and bind its project channel; use project ensure to create or repair a Board")
|
package/dist/commands/reindex.js
CHANGED
|
@@ -47,6 +47,9 @@ const WORKTREE_HOOK_SWEEP_BUDGET_MS = 5_000;
|
|
|
47
47
|
* bounded, which is the point.
|
|
48
48
|
*/
|
|
49
49
|
const HOOK_CHECK_TIMEOUT_MS = 60_000;
|
|
50
|
+
// Leave ten seconds inside the desktop's 30-second package-lease wait for
|
|
51
|
+
// process startup and cleanup when reindex runs from a lifecycle hook.
|
|
52
|
+
const HOOK_CHECK_FROM_HOOK_TIMEOUT_MS = 20_000;
|
|
50
53
|
/** Ceiling override, in milliseconds. Present so tests need not wait a minute. */
|
|
51
54
|
function hookCheckTimeoutMs() {
|
|
52
55
|
const raw = process.env.HQ_HOOK_CHECK_TIMEOUT_MS?.trim();
|
|
@@ -367,11 +370,11 @@ function hasCommandHook(value) {
|
|
|
367
370
|
* an older, unscoped checker — report healthy while the drift only this checker
|
|
368
371
|
* detects goes unseen.
|
|
369
372
|
*/
|
|
370
|
-
function runShippedHookCheck(hqRoot) {
|
|
373
|
+
function runShippedHookCheck(hqRoot, timeoutLimitMs) {
|
|
371
374
|
const checker = path.join(hqRoot, HOOK_CHECK_RELATIVE_PATH);
|
|
372
375
|
if (!fs.existsSync(checker))
|
|
373
376
|
return undefined;
|
|
374
|
-
const budgetMs = hookCheckTimeoutMs();
|
|
377
|
+
const budgetMs = Math.min(hookCheckTimeoutMs(), timeoutLimitMs ?? Number.POSITIVE_INFINITY);
|
|
375
378
|
try {
|
|
376
379
|
const result = spawnSync('bash', [checker, '--root', hqRoot], {
|
|
377
380
|
encoding: 'utf8',
|
|
@@ -406,7 +409,7 @@ function runShippedHookCheck(hqRoot) {
|
|
|
406
409
|
* reindex fires on every Stop / PostToolUse hook, that was the single most
|
|
407
410
|
* expensive discarded result in the command.
|
|
408
411
|
*/
|
|
409
|
-
function inspectHookHealth(hqRoot) {
|
|
412
|
+
function inspectHookHealth(hqRoot, timeoutLimitMs) {
|
|
410
413
|
const settingsPath = path.join(hqRoot, '.claude', 'settings.json');
|
|
411
414
|
if (!fs.existsSync(settingsPath)) {
|
|
412
415
|
return { state: 'extreme', reason: '.claude/settings.json is missing' };
|
|
@@ -435,7 +438,7 @@ function inspectHookHealth(hqRoot) {
|
|
|
435
438
|
reason: `missing command hook wiring for ${HOOK_EVENTS.filter((event) => !commandEvents.includes(event)).join(', ')}`,
|
|
436
439
|
};
|
|
437
440
|
}
|
|
438
|
-
const checkerResult = runShippedHookCheck(hqRoot);
|
|
441
|
+
const checkerResult = runShippedHookCheck(hqRoot, timeoutLimitMs);
|
|
439
442
|
if (checkerResult !== undefined && checkerResult.status !== 0) {
|
|
440
443
|
return {
|
|
441
444
|
state: 'minor',
|
|
@@ -491,12 +494,12 @@ function runSilently(operation) {
|
|
|
491
494
|
* configuration is repaired only after a successful reindex; partial and
|
|
492
495
|
* malformed configurations remain untouched and receive recovery guidance.
|
|
493
496
|
*/
|
|
494
|
-
function repairExtremeHookDrift(hqRoot, allowRepair = true) {
|
|
497
|
+
function repairExtremeHookDrift(hqRoot, allowRepair = true, timeoutLimitMs) {
|
|
495
498
|
// `hq reindex` can still be invoked from an arbitrary directory. Never turn
|
|
496
499
|
// an absent settings file there into a rescue attempt.
|
|
497
500
|
if (!isHqRoot(hqRoot))
|
|
498
501
|
return;
|
|
499
|
-
const health = inspectHookHealth(hqRoot);
|
|
502
|
+
const health = inspectHookHealth(hqRoot, timeoutLimitMs);
|
|
500
503
|
if (health.state === 'healthy')
|
|
501
504
|
return;
|
|
502
505
|
printHookHealthWarning(hqRoot, health.reason);
|
|
@@ -510,7 +513,7 @@ function repairExtremeHookDrift(hqRoot, allowRepair = true) {
|
|
|
510
513
|
paths: ['.claude'],
|
|
511
514
|
assumeYes: true,
|
|
512
515
|
}));
|
|
513
|
-
const afterRepair = inspectHookHealth(hqRoot);
|
|
516
|
+
const afterRepair = inspectHookHealth(hqRoot, timeoutLimitMs);
|
|
514
517
|
if (result.status === 0 && afterRepair.state === 'healthy') {
|
|
515
518
|
console.log('reindex: repaired HQ hook config');
|
|
516
519
|
return;
|
|
@@ -691,7 +694,7 @@ export function registerReindexCommand(program) {
|
|
|
691
694
|
const sweepStartedAt = Date.now();
|
|
692
695
|
removeStaleHqWorktrees(hqRoot, sweepStartedAt, opts.fromHook ? sweepStartedAt + WORKTREE_HOOK_SWEEP_BUDGET_MS : Number.POSITIVE_INFINITY);
|
|
693
696
|
const { status } = reindex({ repoRoot: opts.repoRoot });
|
|
694
|
-
repairExtremeHookDrift(hqRoot, status === 0);
|
|
697
|
+
repairExtremeHookDrift(hqRoot, status === 0, opts.fromHook ? HOOK_CHECK_FROM_HOOK_TIMEOUT_MS : undefined);
|
|
695
698
|
if (status === 0) {
|
|
696
699
|
// Only safe once reindex actually ran: a refused --from-hook reindex
|
|
697
700
|
// means a sync/rescue holds the shared operation lock and may be
|