@indigoai-us/hq-cli 5.166.0 → 5.168.0
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 +32 -0
- package/dist/command-catalog.generated.d.ts +88 -1
- package/dist/command-catalog.generated.js +112 -1
- package/dist/commands/daemon.d.ts +2 -0
- package/dist/commands/daemon.js +79 -22
- package/dist/commands/lanes.js +269 -28
- package/dist/commands/mesh.js +13 -19
- package/dist/commands/outpost-pruner.d.ts +3 -0
- package/dist/commands/outpost-pruner.js +125 -0
- package/dist/lib/daemon/catalog.d.ts +11 -1
- package/dist/lib/daemon/catalog.js +31 -6
- package/dist/lib/daemon/install.d.ts +7 -0
- package/dist/lib/daemon/install.js +18 -2
- package/dist/lib/daemon/legacy.js +6 -1
- package/dist/lib/daemon/machine-type.d.ts +4 -1
- package/dist/lib/daemon/machine-type.js +29 -9
- package/dist/lib/daemon/migrate.js +2 -2
- package/dist/lib/daemon/remote-control.d.ts +32 -9
- package/dist/lib/daemon/remote-control.js +55 -11
- package/dist/lib/daemon/run.d.ts +6 -0
- package/dist/lib/daemon/run.js +43 -2
- package/dist/lib/daemon/tools.d.ts +52 -0
- package/dist/lib/daemon/tools.js +95 -0
- package/dist/lib/daemon/updater.d.ts +23 -15
- package/dist/lib/daemon/updater.js +97 -60
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/outpost-dispatch.d.ts +1 -1
- package/dist/lib/lanes/outpost-pruner-install.d.ts +37 -0
- package/dist/lib/lanes/outpost-pruner-install.js +226 -0
- package/dist/lib/lanes/outpost-worktree-pruner-flag.d.ts +19 -0
- package/dist/lib/lanes/outpost-worktree-pruner-flag.js +76 -0
- package/dist/lib/lanes/outpost-worktree-pruner.d.ts +60 -0
- package/dist/lib/lanes/outpost-worktree-pruner.js +1135 -0
- package/dist/lib/lanes/store.d.ts +6 -0
- package/dist/lib/lanes/store.js +66 -1
- package/dist/lib/mesh/live/session-events-client.js +3 -2
- package/dist/lib/mesh/project-board.d.ts +7 -0
- package/dist/lib/mesh/project-board.js +29 -1
- package/package.json +1 -1
|
@@ -54,6 +54,12 @@ export declare function tryReadQuestion(hqRoot: string, questionId: string, deps
|
|
|
54
54
|
export declare function readQuestion(hqRoot: string, questionId: string, deps?: StoreDeps): QuestionRecord;
|
|
55
55
|
export declare function createLane(hqRoot: string, init: Omit<LaneRecord, "schema_version" | "lane_id" | "version" | "created_at" | "updated_at"> & Partial<Pick<LaneRecord, "schema_version" | "lane_id" | "version" | "created_at" | "updated_at">>, deps?: StoreDeps): LaneRecord;
|
|
56
56
|
export declare function updateLane(hqRoot: string, laneId: string, expectedVersion: number, writer: LaneWriter, updater: (current: LaneRecord) => LaneRecord, deps?: StoreDeps): LaneRecord;
|
|
57
|
+
/**
|
|
58
|
+
* Replace the primary copy with a terminal snapshot returned by the Outpost
|
|
59
|
+
* that owns the lane. The owner is authoritative on equal versions; a newer
|
|
60
|
+
* primary version is a conflict and must not be overwritten.
|
|
61
|
+
*/
|
|
62
|
+
export declare function syncLaneRecordFromOwner(hqRoot: string, laneId: string, outpostId: string, rawOwnerLane: unknown, deps?: StoreDeps): LaneRecord;
|
|
57
63
|
/** Compare-and-set with conflict retry. Callers that race a concurrent writer re-read. */
|
|
58
64
|
export declare function retryUpdateLane(hqRoot: string, laneId: string, writer: LaneWriter, updater: (current: LaneRecord) => LaneRecord, deps?: StoreDeps, attempts?: number): LaneRecord;
|
|
59
65
|
/** Compare-and-set with conflict retry for question records (spec §6.2). */
|
package/dist/lib/lanes/store.js
CHANGED
|
@@ -13,7 +13,7 @@ import * as path from "node:path";
|
|
|
13
13
|
import { generateUlid } from "../mesh/live/ulid.js";
|
|
14
14
|
import { resolveLanesHqRoot } from "./hq-root.js";
|
|
15
15
|
import { LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesError, isLaneVersionConflict, } from "./errors.js";
|
|
16
|
-
import { HISTORY_CAP, HOST_KINDS, LANE_STORY_STATUSES, LANE_SCHEMA_VERSION, QUESTION_SCHEMA_VERSION, SENIOR_KINDS, isHostKind, isLaneProvider, isQueuedReason, isLaneState, isQuestionStatus, isSeniorKind, parseMajorSchema, REQUIRED_LANE_RECORD_FIELDS, LANE_FIELD_WRITERS, LANE_WRITERS, } from "./types.js";
|
|
16
|
+
import { HISTORY_CAP, HOST_KINDS, LANE_STORY_STATUSES, LANE_SCHEMA_VERSION, QUESTION_SCHEMA_VERSION, SENIOR_KINDS, isHostKind, isLaneProvider, isQueuedReason, isLaneState, isQuestionStatus, isSeniorKind, isTerminalLaneState, parseMajorSchema, REQUIRED_LANE_RECORD_FIELDS, LANE_FIELD_WRITERS, LANE_WRITERS, } from "./types.js";
|
|
17
17
|
import { laneStories } from "./stories.js";
|
|
18
18
|
export const LANES_STORE_REL = path.join("workspace", "lanes");
|
|
19
19
|
export const LANE_RECORDS_DIR = "lanes";
|
|
@@ -810,6 +810,71 @@ export function updateLane(hqRoot, laneId, expectedVersion, writer, updater, dep
|
|
|
810
810
|
return updated;
|
|
811
811
|
}, deps);
|
|
812
812
|
}
|
|
813
|
+
/**
|
|
814
|
+
* Replace the primary copy with a terminal snapshot returned by the Outpost
|
|
815
|
+
* that owns the lane. The owner is authoritative on equal versions; a newer
|
|
816
|
+
* primary version is a conflict and must not be overwritten.
|
|
817
|
+
*/
|
|
818
|
+
export function syncLaneRecordFromOwner(hqRoot, laneId, outpostId, rawOwnerLane, deps = defaultStoreDeps()) {
|
|
819
|
+
let ownerLane;
|
|
820
|
+
try {
|
|
821
|
+
ownerLane = parseLaneRecord(rawOwnerLane);
|
|
822
|
+
}
|
|
823
|
+
catch (err) {
|
|
824
|
+
throw new LanesError("outpost_lane_sync_invalid", `Outpost ${outpostId} returned an invalid lane snapshot for ${laneId}`, {
|
|
825
|
+
lane_id: laneId,
|
|
826
|
+
outpost_id: outpostId,
|
|
827
|
+
cause: err instanceof Error ? err.message : String(err),
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
if (ownerLane.lane_id !== laneId ||
|
|
831
|
+
ownerLane.host.kind !== "outpost" ||
|
|
832
|
+
ownerLane.host.id !== outpostId ||
|
|
833
|
+
!isTerminalLaneState(ownerLane.state)) {
|
|
834
|
+
throw new LanesError("outpost_lane_sync_invalid", `Outpost ${outpostId} did not return its terminal record for lane ${laneId}`, {
|
|
835
|
+
lane_id: laneId,
|
|
836
|
+
outpost_id: outpostId,
|
|
837
|
+
owner_lane_id: ownerLane.lane_id,
|
|
838
|
+
owner_host: ownerLane.host,
|
|
839
|
+
owner_state: ownerLane.state,
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
const filePath = laneRecordPath(hqRoot, laneId);
|
|
843
|
+
return withRecordLock(filePath, deps, () => {
|
|
844
|
+
const current = readMergedRecord(filePath, parseLaneRecord, mergeLaneRecords, deps);
|
|
845
|
+
if (current === null) {
|
|
846
|
+
throw new LaneSchemaError(`Record disappeared under lock: ${filePath}`, {
|
|
847
|
+
path: filePath,
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
if (current.lane_id !== laneId ||
|
|
851
|
+
current.host.kind !== "outpost" ||
|
|
852
|
+
current.host.id !== outpostId) {
|
|
853
|
+
throw new LanesError("outpost_lane_sync_conflict", `Primary lane ${laneId} is no longer owned by Outpost ${outpostId}`, {
|
|
854
|
+
lane_id: laneId,
|
|
855
|
+
outpost_id: outpostId,
|
|
856
|
+
current_lane_id: current.lane_id,
|
|
857
|
+
current_host: current.host,
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
if (ownerLane.version < current.version) {
|
|
861
|
+
throw new LanesError("outpost_lane_sync_conflict", `Outpost ${outpostId} returned lane ${laneId} at version ${ownerLane.version}, behind primary version ${current.version}`, {
|
|
862
|
+
lane_id: laneId,
|
|
863
|
+
outpost_id: outpostId,
|
|
864
|
+
owner_version: ownerLane.version,
|
|
865
|
+
primary_version: current.version,
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
const ownerSnapshot = ownerLane.outpost_command_id === undefined &&
|
|
869
|
+
current.outpost_command_id !== undefined
|
|
870
|
+
? { ...ownerLane, outpost_command_id: current.outpost_command_id }
|
|
871
|
+
: ownerLane;
|
|
872
|
+
const synced = mergeLaneRecords(ownerSnapshot, [current]);
|
|
873
|
+
parseLaneRecord(synced, filePath);
|
|
874
|
+
atomicWriteJson(filePath, synced, deps);
|
|
875
|
+
return synced;
|
|
876
|
+
});
|
|
877
|
+
}
|
|
813
878
|
/** Compare-and-set with conflict retry. Callers that race a concurrent writer re-read. */
|
|
814
879
|
export function retryUpdateLane(hqRoot, laneId, writer, updater, deps = defaultStoreDeps(), attempts = 5) {
|
|
815
880
|
let last;
|
|
@@ -22,14 +22,15 @@ function parseRetryAfterMs(value, nowMs = Date.now()) {
|
|
|
22
22
|
const trimmed = value.trim();
|
|
23
23
|
if (/^\d+$/.test(trimmed)) {
|
|
24
24
|
const delayMs = Number(trimmed) * 1_000;
|
|
25
|
-
return Number.isSafeInteger(delayMs) ? delayMs : undefined;
|
|
25
|
+
return Number.isSafeInteger(delayMs) && delayMs > 0 ? delayMs : undefined;
|
|
26
26
|
}
|
|
27
27
|
if (/^[+-]/.test(trimmed))
|
|
28
28
|
return undefined;
|
|
29
29
|
const retryAtMs = Date.parse(trimmed);
|
|
30
30
|
if (!Number.isFinite(retryAtMs))
|
|
31
31
|
return undefined;
|
|
32
|
-
|
|
32
|
+
const delayMs = retryAtMs - nowMs;
|
|
33
|
+
return delayMs > 0 ? delayMs : undefined;
|
|
33
34
|
}
|
|
34
35
|
function retryAfterForStatus(status, headers) {
|
|
35
36
|
if (status !== 429 && status !== 503)
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { type MeshCompany, type MeshProjectRegistration, type MeshProjectRepo, type MeshProjectStoryInput, type MeshProjectView } from "./api.js";
|
|
2
|
+
/** Same merge as `hq mesh project ensure`: add missing stories, keep live status. */
|
|
3
|
+
export declare function mergeProjectStories(current: MeshProjectStoryInput[], planned: MeshProjectStoryInput[], opts?: {
|
|
4
|
+
resetStatus?: boolean;
|
|
5
|
+
}): MeshProjectStoryInput[];
|
|
6
|
+
export declare function boardStoryStatus(story: MeshProjectStoryInput): string;
|
|
7
|
+
/** One-line summary of live→next status diffs for existing story ids. */
|
|
8
|
+
export declare function formatStoryStatusChanges(current: MeshProjectStoryInput[], next: MeshProjectStoryInput[]): string | undefined;
|
|
2
9
|
/**
|
|
3
10
|
* Add Board stories that a prd-sync emit is about to name, when they are missing.
|
|
4
11
|
* A missing view uses ensureProjectBoard. An existing view is merged so live
|
|
@@ -6,13 +6,19 @@ function httpStatus(err) {
|
|
|
6
6
|
return undefined;
|
|
7
7
|
}
|
|
8
8
|
/** Same merge as `hq mesh project ensure`: add missing stories, keep live status. */
|
|
9
|
-
function mergeProjectStories(current, planned) {
|
|
9
|
+
export function mergeProjectStories(current, planned, opts) {
|
|
10
10
|
const currentById = new Map(current.map((story) => [story.id, story]));
|
|
11
11
|
const plannedIds = new Set(planned.map((story) => story.id));
|
|
12
12
|
const merged = planned.map((story) => {
|
|
13
13
|
const live = currentById.get(story.id);
|
|
14
14
|
if (!live)
|
|
15
15
|
return story;
|
|
16
|
+
if (opts?.resetStatus) {
|
|
17
|
+
return {
|
|
18
|
+
...story,
|
|
19
|
+
...(live.assigneeUid ? { assigneeUid: live.assigneeUid } : {}),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
16
22
|
const status = live.status || story.status;
|
|
17
23
|
return {
|
|
18
24
|
...story,
|
|
@@ -23,6 +29,28 @@ function mergeProjectStories(current, planned) {
|
|
|
23
29
|
});
|
|
24
30
|
return [...merged, ...current.filter((story) => !plannedIds.has(story.id))];
|
|
25
31
|
}
|
|
32
|
+
export function boardStoryStatus(story) {
|
|
33
|
+
if (story.status)
|
|
34
|
+
return story.status;
|
|
35
|
+
return story.passes === true ? "done" : "queued";
|
|
36
|
+
}
|
|
37
|
+
/** One-line summary of live→next status diffs for existing story ids. */
|
|
38
|
+
export function formatStoryStatusChanges(current, next) {
|
|
39
|
+
const currentById = new Map(current.map((story) => [story.id, story]));
|
|
40
|
+
const changes = [];
|
|
41
|
+
for (const story of next) {
|
|
42
|
+
const live = currentById.get(story.id);
|
|
43
|
+
if (!live)
|
|
44
|
+
continue;
|
|
45
|
+
const from = boardStoryStatus(live);
|
|
46
|
+
const to = boardStoryStatus(story);
|
|
47
|
+
if (from !== to)
|
|
48
|
+
changes.push(`${story.id} ${from}→${to}`);
|
|
49
|
+
}
|
|
50
|
+
if (changes.length === 0)
|
|
51
|
+
return undefined;
|
|
52
|
+
return `Work mesh: project set changed status: ${changes.join(", ")}`;
|
|
53
|
+
}
|
|
26
54
|
function mergeProjectRepos(current, planned) {
|
|
27
55
|
const plannedPaths = new Set(planned.map((repo) => repo.path));
|
|
28
56
|
return [...planned, ...current.filter((repo) => !plannedPaths.has(repo.path))];
|