@indigoai-us/hq-cli 5.193.0 → 5.193.1
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 +13 -0
- package/dist/command-catalog.generated.d.ts +3 -0
- package/dist/command-catalog.generated.js +4 -0
- package/dist/commands/lanes.js +26 -5
- package/dist/commands/mesh.js +45 -6
- package/dist/lib/mesh/live/daemon/board-refresh.js +2 -1
- package/dist/lib/mesh/project-board.js +5 -3
- package/dist/lib/mesh/story-status.d.ts +22 -0
- package/dist/lib/mesh/story-status.js +49 -0
- package/dist/lib/work-context/prd-sync.d.ts +1 -1
- package/dist/lib/work-context/prd-sync.js +8 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.193.1] — 2026-09-25
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Work Mesh project sync maps ledger states (`todo` to `queued`, `in_progress` to
|
|
10
|
+
`in_progress`, `in_review`/`verification`/`blocked` to `review`, and `done`/`dropped`
|
|
11
|
+
to `done`), with explicit `status` taking precedence and `passes` preserved. PRD
|
|
12
|
+
sync emits review transitions; Board readers preserve unknown status strings and
|
|
13
|
+
ignore unknown ledger states. `hq mesh project ensure --dry-run` previews merged
|
|
14
|
+
Board statuses without writing.
|
|
15
|
+
- Claude lane balance reports now normalize Outpost lane-list and queued show
|
|
16
|
+
views before strict lane-record validation, including `no_story_reason: null`.
|
|
17
|
+
|
|
5
18
|
## [5.193.0] — 2026-09-25
|
|
6
19
|
|
|
7
20
|
### Fixed
|
|
@@ -5777,6 +5777,9 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
5777
5777
|
}, {
|
|
5778
5778
|
readonly flags: "--stories-file <path>";
|
|
5779
5779
|
readonly description: "PRD JSON file with userStories and metadata.repos";
|
|
5780
|
+
}, {
|
|
5781
|
+
readonly flags: "--dry-run";
|
|
5782
|
+
readonly description: "Read the current Board and print computed story statuses without writing";
|
|
5780
5783
|
}, {
|
|
5781
5784
|
readonly flags: "--json";
|
|
5782
5785
|
readonly description: "Print {projectId, threadId, channelId, version, stories}";
|
|
@@ -7458,6 +7458,10 @@ export const COMMAND_CATALOG = [
|
|
|
7458
7458
|
"flags": "--stories-file <path>",
|
|
7459
7459
|
"description": "PRD JSON file with userStories and metadata.repos"
|
|
7460
7460
|
},
|
|
7461
|
+
{
|
|
7462
|
+
"flags": "--dry-run",
|
|
7463
|
+
"description": "Read the current Board and print computed story statuses without writing"
|
|
7464
|
+
},
|
|
7461
7465
|
{
|
|
7462
7466
|
"flags": "--json",
|
|
7463
7467
|
"description": "Print {projectId, threadId, channelId, version, stories}"
|
package/dist/commands/lanes.js
CHANGED
|
@@ -534,6 +534,30 @@ function ownerErrorFromOutput(stdout) {
|
|
|
534
534
|
: `Outpost owner returned ${owner.error}`;
|
|
535
535
|
return { error: owner.error, message, details };
|
|
536
536
|
}
|
|
537
|
+
const LANE_RECORD_VIEW_FIELDS = [
|
|
538
|
+
"story_ids",
|
|
539
|
+
"hire_alive_children",
|
|
540
|
+
"hire",
|
|
541
|
+
"hire_summary",
|
|
542
|
+
"hire_source",
|
|
543
|
+
"hire_warning",
|
|
544
|
+
"senior_lost",
|
|
545
|
+
"remote_state",
|
|
546
|
+
];
|
|
547
|
+
/** Remove presentation-only fields before validating a remote lane snapshot. */
|
|
548
|
+
function normalizeLaneRecordView(raw) {
|
|
549
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
550
|
+
return raw;
|
|
551
|
+
const normalized = { ...raw };
|
|
552
|
+
if (normalized.state === "remote" && normalized.remote_state !== undefined) {
|
|
553
|
+
normalized.state = normalized.remote_state;
|
|
554
|
+
}
|
|
555
|
+
for (const field of LANE_RECORD_VIEW_FIELDS)
|
|
556
|
+
delete normalized[field];
|
|
557
|
+
if (normalized.no_story_reason === null)
|
|
558
|
+
delete normalized.no_story_reason;
|
|
559
|
+
return normalized;
|
|
560
|
+
}
|
|
537
561
|
async function ownerLaneHireFields(ctx, lane) {
|
|
538
562
|
if (lane.host.kind !== "outpost") {
|
|
539
563
|
throw new LanesError("outpost_lane_hire_summary_invalid", "Owner summary requires an Outpost-hosted lane", {
|
|
@@ -567,7 +591,7 @@ async function ownerLaneHireFields(ctx, lane) {
|
|
|
567
591
|
}
|
|
568
592
|
let ownerLane;
|
|
569
593
|
try {
|
|
570
|
-
ownerLane = parseLaneRecord(remote.lane);
|
|
594
|
+
ownerLane = parseLaneRecord(normalizeLaneRecordView(remote.lane));
|
|
571
595
|
}
|
|
572
596
|
catch (err) {
|
|
573
597
|
throw new LanesError("outpost_lane_hire_summary_invalid", `Outpost ${outpostId} returned an invalid lane snapshot for ${lane.lane_id}`, { lane_id: lane.lane_id, outpost_id: outpostId, cause: dispatchErrorMessage(err) });
|
|
@@ -890,11 +914,8 @@ async function readOutpostLaneRecords(ctx, outpostId) {
|
|
|
890
914
|
typeof rowHost.id === "string" && rowHost.id.trim() !== "";
|
|
891
915
|
if (!belongsToOutpost && !hasLocalHostIdentity)
|
|
892
916
|
continue;
|
|
893
|
-
const normalized = raw.state === "remote" && raw.remote_state !== undefined
|
|
894
|
-
? { ...raw, state: raw.remote_state }
|
|
895
|
-
: raw;
|
|
896
917
|
try {
|
|
897
|
-
lanes.push(parseLaneRecord(
|
|
918
|
+
lanes.push(parseLaneRecord(normalizeLaneRecordView(raw)));
|
|
898
919
|
}
|
|
899
920
|
catch (err) {
|
|
900
921
|
throw new LanesError("outpost_lane_list_invalid", `Outpost ${outpostId} returned an invalid owned lane record: ${dispatchErrorMessage(err)}`, { outpost_id: outpostId, index });
|
package/dist/commands/mesh.js
CHANGED
|
@@ -14,7 +14,8 @@ import chalk from "chalk";
|
|
|
14
14
|
import { loadCachedTokens, isMachineIdentity } from "../utils/cognito-session.js";
|
|
15
15
|
import * as readline from "node:readline/promises";
|
|
16
16
|
import { STORY_STATUSES, appendThreadEvent, ensureProjectThread, eventPayload, getProjectView, listActiveMembershipCompanies, listActiveThreads, patchStory, putProjectView, registerMeshProject, resolveActiveMembershipCompany, resolveMeshPrincipalUid, warmMeshConversationCache, } from "../lib/mesh/api.js";
|
|
17
|
-
import { closeSupersededProjectThread, ensureMissingBoardStories, ensureProjectBoard, formatStoryStatusChanges, mergeProjectStories, prdSyncEventsThatChangeBoard, } from "../lib/mesh/project-board.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";
|
|
18
19
|
import { buildPreflightFixtures } from "../lib/work-context/preflight-contract.js";
|
|
19
20
|
import { createCandidatesFetcher, createMigratePoster, createOrganizePoster, createWorkSessionDeliverer, fetchCompanyLive, formatCompanyLiveTable, openMeshTransport, probeMigrationCapabilityForMemberships, requireToken, } from "../lib/mesh/client.js";
|
|
20
21
|
import { clearDefaultCompany, isDefaultCompanyDisabled, readDeviceConfig, recordMigrationCapabilitySnapshot, setRepairHeldWithDefault, setDefaultCompany, } from "../lib/work-context/config.js";
|
|
@@ -258,10 +259,7 @@ function projectStoriesFromJson(value, source) {
|
|
|
258
259
|
if (!story || !id || !title) {
|
|
259
260
|
fail(`${source} userStories[${index}] requires string id and title.`);
|
|
260
261
|
}
|
|
261
|
-
const status =
|
|
262
|
-
if (status && !STORY_STATUSES.has(status)) {
|
|
263
|
-
fail(`${source} userStories[${index}].status must be queued|in_progress|review|done.`);
|
|
264
|
-
}
|
|
262
|
+
const status = mapSourceStoryStatus(story, `${source} userStories[${index}]`);
|
|
265
263
|
const resolvedStatus = status || (story.passes === true ? "done" : undefined);
|
|
266
264
|
return {
|
|
267
265
|
id,
|
|
@@ -475,6 +473,42 @@ async function runProjectEnsure(projectId, opts) {
|
|
|
475
473
|
if (httpStatus(err) !== 404)
|
|
476
474
|
throw err;
|
|
477
475
|
}
|
|
476
|
+
if (opts.dryRun) {
|
|
477
|
+
if (current && (current.companyUid !== company.companyUid || current.projectId !== projectId)) {
|
|
478
|
+
fail("Work mesh project ensure dry-run: Board company or project id did not match the request.");
|
|
479
|
+
}
|
|
480
|
+
const stories = current ? mergeProjectStories(current.stories, source.stories) : source.stories;
|
|
481
|
+
const sourceStatuses = source.stories.map((story) => ({
|
|
482
|
+
id: story.id,
|
|
483
|
+
title: story.title,
|
|
484
|
+
status: boardStoryStatus(story),
|
|
485
|
+
}));
|
|
486
|
+
const boardStatuses = stories.map((story) => ({
|
|
487
|
+
id: story.id,
|
|
488
|
+
title: story.title,
|
|
489
|
+
status: boardStoryStatus(story),
|
|
490
|
+
}));
|
|
491
|
+
if (opts.json) {
|
|
492
|
+
console.log(JSON.stringify({
|
|
493
|
+
ok: true,
|
|
494
|
+
action: "project.ensure.dry-run",
|
|
495
|
+
projectId,
|
|
496
|
+
companyUid: company.companyUid,
|
|
497
|
+
sourceStatuses,
|
|
498
|
+
stories: boardStatuses,
|
|
499
|
+
}, null, 2));
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
console.log(`Work mesh: project ${projectId} dry run (${sourceStatuses.length} source stories)`);
|
|
503
|
+
console.log("Source statuses:");
|
|
504
|
+
for (const story of sourceStatuses)
|
|
505
|
+
console.log(`${story.id}\t${story.status}`);
|
|
506
|
+
console.log("Merged Board statuses:");
|
|
507
|
+
for (const story of boardStatuses)
|
|
508
|
+
console.log(`${story.id}\t${story.status}`);
|
|
509
|
+
}
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
478
512
|
// Genesis thread, then the view, then register. Register returns 403
|
|
479
513
|
// PROJECT_NOT_IN_COMPANY until both exist. A missing view uses that chain
|
|
480
514
|
// from ensureProjectBoard; an existing view is merged, then registered.
|
|
@@ -659,7 +693,11 @@ async function runPrdSync(opts) {
|
|
|
659
693
|
source: "hooks",
|
|
660
694
|
taskId: event.taskId,
|
|
661
695
|
status: event.status,
|
|
662
|
-
summary: event.status === "done"
|
|
696
|
+
summary: event.status === "done"
|
|
697
|
+
? `${event.taskId} passes`
|
|
698
|
+
: event.status === "in_progress"
|
|
699
|
+
? `${event.taskId} in progress`
|
|
700
|
+
: `${event.taskId} in review`,
|
|
663
701
|
evidence: { ...evidenceBase, task: event.taskId },
|
|
664
702
|
}));
|
|
665
703
|
const poster = createEmitPoster({ token, baseUrl: resolveVaultApiBase(process.env) });
|
|
@@ -1855,6 +1893,7 @@ export function registerMeshCommand(program) {
|
|
|
1855
1893
|
.description("Idempotently register a PRD on the Board and verify the view")
|
|
1856
1894
|
.requiredOption("--company <slug|uid>", "Company slug or cloud uid")
|
|
1857
1895
|
.requiredOption("--stories-file <path>", "PRD JSON file with userStories and metadata.repos")
|
|
1896
|
+
.option("--dry-run", "Read the current Board and print computed story statuses without writing")
|
|
1858
1897
|
.option("--json", "Print {projectId, threadId, channelId, version, stories}")
|
|
1859
1898
|
.action((projectId, opts) => wrap(() => runProjectEnsure(projectId, opts))());
|
|
1860
1899
|
project
|
|
@@ -10,6 +10,7 @@ import * as fs from "node:fs";
|
|
|
10
10
|
import * as path from "node:path";
|
|
11
11
|
import { workContextSessionsDir } from "../../../work-context/paths.js";
|
|
12
12
|
import { readSessionState } from "../../../work-context/state.js";
|
|
13
|
+
import { mapReaderStoryStatus } from "../../story-status.js";
|
|
13
14
|
import { sessionBoardPath } from "./paths.js";
|
|
14
15
|
export const BOARD_REFRESH_INTERVAL_MS = 60_000;
|
|
15
16
|
function listBoundSessionIds(workContextRoot) {
|
|
@@ -147,7 +148,7 @@ function storiesFromProject(project) {
|
|
|
147
148
|
: typeof row.name === "string"
|
|
148
149
|
? row.name.slice(0, 120)
|
|
149
150
|
: undefined,
|
|
150
|
-
status:
|
|
151
|
+
status: mapReaderStoryStatus(row),
|
|
151
152
|
});
|
|
152
153
|
}
|
|
153
154
|
return stories;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { appendThreadEvent, ensureProjectThread, getProjectView, putProjectView, registerMeshProject, } from "./api.js";
|
|
2
|
+
import { mapReaderStoryStatus } from "./story-status.js";
|
|
2
3
|
function httpStatus(err) {
|
|
3
4
|
if (err && typeof err === "object" && "status" in err && typeof err.status === "number") {
|
|
4
5
|
return err.status;
|
|
@@ -19,7 +20,7 @@ export function mergeProjectStories(current, planned, opts) {
|
|
|
19
20
|
...(live.assigneeUid ? { assigneeUid: live.assigneeUid } : {}),
|
|
20
21
|
};
|
|
21
22
|
}
|
|
22
|
-
const status = live
|
|
23
|
+
const status = mapReaderStoryStatus(live) || story.status;
|
|
23
24
|
return {
|
|
24
25
|
...story,
|
|
25
26
|
...(status ? { status } : {}),
|
|
@@ -30,8 +31,9 @@ export function mergeProjectStories(current, planned, opts) {
|
|
|
30
31
|
return [...merged, ...current.filter((story) => !plannedIds.has(story.id))];
|
|
31
32
|
}
|
|
32
33
|
export function boardStoryStatus(story) {
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
const status = mapReaderStoryStatus(story);
|
|
35
|
+
if (status)
|
|
36
|
+
return status;
|
|
35
37
|
return story.passes === true ? "done" : "queued";
|
|
36
38
|
}
|
|
37
39
|
/**
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const BOARD_STORY_STATUSES: readonly ["queued", "in_progress", "review", "done"];
|
|
2
|
+
export type BoardStoryStatus = (typeof BOARD_STORY_STATUSES)[number];
|
|
3
|
+
/**
|
|
4
|
+
* The ledger has more states than the Board. `blocked` and the review states
|
|
5
|
+
* need attention, while `dropped` is terminal; the Board has only one terminal
|
|
6
|
+
* column (`done`).
|
|
7
|
+
*/
|
|
8
|
+
export declare const LEDGER_STATE_TO_BOARD_STATUS: Readonly<Record<string, BoardStoryStatus>>;
|
|
9
|
+
export declare function mapSourceStoryStatus(story: {
|
|
10
|
+
status?: unknown;
|
|
11
|
+
state?: unknown;
|
|
12
|
+
}, source: string): BoardStoryStatus | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Read a status from an existing PRD or Board response without imposing the
|
|
15
|
+
* stricter `--stories-file` contract. Before state mapping was added, these
|
|
16
|
+
* readers passed explicit status strings through and ignored state.
|
|
17
|
+
*/
|
|
18
|
+
export declare function mapReaderStoryStatus(story: {
|
|
19
|
+
status?: unknown;
|
|
20
|
+
state?: unknown;
|
|
21
|
+
}): string | undefined;
|
|
22
|
+
//# sourceMappingURL=story-status.d.ts.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export const BOARD_STORY_STATUSES = ["queued", "in_progress", "review", "done"];
|
|
2
|
+
const boardStoryStatusSet = new Set(BOARD_STORY_STATUSES);
|
|
3
|
+
/**
|
|
4
|
+
* The ledger has more states than the Board. `blocked` and the review states
|
|
5
|
+
* need attention, while `dropped` is terminal; the Board has only one terminal
|
|
6
|
+
* column (`done`).
|
|
7
|
+
*/
|
|
8
|
+
export const LEDGER_STATE_TO_BOARD_STATUS = {
|
|
9
|
+
todo: "queued",
|
|
10
|
+
in_progress: "in_progress",
|
|
11
|
+
in_review: "review",
|
|
12
|
+
verification: "review",
|
|
13
|
+
blocked: "review",
|
|
14
|
+
done: "done",
|
|
15
|
+
dropped: "done",
|
|
16
|
+
};
|
|
17
|
+
export function mapSourceStoryStatus(story, source) {
|
|
18
|
+
if (story.status !== undefined && story.status !== null) {
|
|
19
|
+
if (typeof story.status !== "string" || !boardStoryStatusSet.has(story.status.trim())) {
|
|
20
|
+
throw new Error(`${source}.status must be queued|in_progress|review|done.`);
|
|
21
|
+
}
|
|
22
|
+
return story.status.trim();
|
|
23
|
+
}
|
|
24
|
+
if (Object.hasOwn(story, "state")) {
|
|
25
|
+
if (typeof story.state !== "string" ||
|
|
26
|
+
!Object.hasOwn(LEDGER_STATE_TO_BOARD_STATUS, story.state.trim())) {
|
|
27
|
+
throw new Error(`${source}.state must be todo|in_progress|in_review|verification|blocked|done|dropped.`);
|
|
28
|
+
}
|
|
29
|
+
return LEDGER_STATE_TO_BOARD_STATUS[story.state.trim()];
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Read a status from an existing PRD or Board response without imposing the
|
|
35
|
+
* stricter `--stories-file` contract. Before state mapping was added, these
|
|
36
|
+
* readers passed explicit status strings through and ignored state.
|
|
37
|
+
*/
|
|
38
|
+
export function mapReaderStoryStatus(story) {
|
|
39
|
+
if (story.status !== undefined && story.status !== null) {
|
|
40
|
+
return typeof story.status === "string" ? story.status : undefined;
|
|
41
|
+
}
|
|
42
|
+
if (typeof story.state !== "string")
|
|
43
|
+
return undefined;
|
|
44
|
+
const state = story.state.trim();
|
|
45
|
+
return Object.hasOwn(LEDGER_STATE_TO_BOARD_STATUS, state)
|
|
46
|
+
? LEDGER_STATE_TO_BOARD_STATUS[state]
|
|
47
|
+
: undefined;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=story-status.js.map
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import * as fs from "node:fs";
|
|
9
9
|
import * as path from "node:path";
|
|
10
10
|
import { readSessionState, updateSessionState } from "./state.js";
|
|
11
|
+
import { mapReaderStoryStatus } from "../mesh/story-status.js";
|
|
11
12
|
export function prdSnapshotPath(sessionId, root) {
|
|
12
13
|
return path.join(root, "sessions", `${sessionId}.prd-snapshot.json`);
|
|
13
14
|
}
|
|
@@ -87,11 +88,12 @@ function storiesFromPrd(file) {
|
|
|
87
88
|
const id = row.id;
|
|
88
89
|
if (typeof id !== "string" || !id.trim())
|
|
89
90
|
continue;
|
|
90
|
-
const
|
|
91
|
+
const rawStatus = mapReaderStoryStatus(row);
|
|
92
|
+
const status = rawStatus?.trim() ? rawStatus.trim() : undefined;
|
|
91
93
|
out.push({
|
|
92
94
|
id: id.trim(),
|
|
93
95
|
passes: row.passes === true,
|
|
94
|
-
...(
|
|
96
|
+
...(status ? { status } : {}),
|
|
95
97
|
});
|
|
96
98
|
}
|
|
97
99
|
return out;
|
|
@@ -144,7 +146,7 @@ export function planPrdSync(input) {
|
|
|
144
146
|
...(story.status ? { status: story.status } : {}),
|
|
145
147
|
};
|
|
146
148
|
const prior = previous ? before[story.id] : undefined;
|
|
147
|
-
if (story.passes && !prior?.passes) {
|
|
149
|
+
if ((story.passes || story.status === "done") && !prior?.passes && prior?.status !== "done") {
|
|
148
150
|
events.push({ taskId: story.id, status: "done" });
|
|
149
151
|
continue;
|
|
150
152
|
}
|
|
@@ -152,6 +154,9 @@ export function planPrdSync(input) {
|
|
|
152
154
|
events.push({ taskId: story.id, status: "in_progress" });
|
|
153
155
|
taskId = story.id;
|
|
154
156
|
}
|
|
157
|
+
if (story.status === "review" && prior?.status !== "review") {
|
|
158
|
+
events.push({ taskId: story.id, status: "review" });
|
|
159
|
+
}
|
|
155
160
|
}
|
|
156
161
|
const snapshot = {
|
|
157
162
|
stories: nextStories,
|