@indigoai-us/hq-cli 5.147.0 → 5.149.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 +29 -0
- package/assets/scaffold/core/scripts/checkpoint-stop-gate.sh +8 -11
- package/dist/command-catalog.generated.d.ts +129 -0
- package/dist/command-catalog.generated.js +168 -0
- package/dist/commands/lanes.js +506 -23
- package/dist/commands/mcp-registration.d.ts +29 -16
- package/dist/commands/mcp-registration.js +254 -17
- package/dist/commands/mesh.js +82 -11
- package/dist/lib/lanes/index.d.ts +3 -2
- package/dist/lib/lanes/index.js +2 -1
- package/dist/lib/lanes/mesh-events.d.ts +8 -0
- package/dist/lib/lanes/mesh-events.js +83 -6
- package/dist/lib/lanes/outpost-dispatch.d.ts +12 -0
- package/dist/lib/lanes/outpost-dispatch.js +17 -0
- package/dist/lib/lanes/reconcile.js +39 -12
- package/dist/lib/lanes/session-relink.d.ts +25 -0
- package/dist/lib/lanes/session-relink.js +216 -0
- package/dist/lib/lanes/spawn.d.ts +1 -0
- package/dist/lib/lanes/spawn.js +48 -5
- package/dist/lib/lanes/store.js +109 -1
- package/dist/lib/lanes/stories.d.ts +17 -0
- package/dist/lib/lanes/stories.js +73 -0
- package/dist/lib/lanes/types.d.ts +20 -0
- package/dist/lib/lanes/types.js +9 -2
- package/dist/lib/lanes/watcher.js +69 -9
- package/dist/lib/mesh/live/daemon/doctor.d.ts +27 -0
- package/dist/lib/mesh/live/daemon/doctor.js +84 -0
- package/dist/lib/mesh/live/daemon/index.d.ts +3 -3
- package/dist/lib/mesh/live/daemon/index.js +2 -2
- package/dist/lib/mesh/live/daemon/install.d.ts +40 -0
- package/dist/lib/mesh/live/daemon/install.js +88 -0
- package/dist/lib/mesh/live/daemon/run.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/run.js +12 -1
- package/dist/lib/mesh/live/daemon/state.d.ts +4 -0
- package/dist/lib/mesh/project-board.d.ts +15 -0
- package/dist/lib/mesh/project-board.js +84 -0
- package/dist/lib/work-context/prd-sync.d.ts +5 -1
- package/dist/lib/work-context/prd-sync.js +22 -1
- package/dist/sentry.js +15 -7
- package/dist/utils/sentry-fingerprint.d.ts +11 -0
- package/dist/utils/sentry-fingerprint.js +56 -11
- package/package.json +5 -3
- package/scripts/mesh-daemon-if-stale.mjs +16 -0
package/dist/lib/lanes/store.js
CHANGED
|
@@ -13,7 +13,8 @@ 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_SCHEMA_VERSION, QUESTION_SCHEMA_VERSION, SENIOR_KINDS, isHostKind, isLaneProvider, 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, isLaneState, isQuestionStatus, isSeniorKind, parseMajorSchema, REQUIRED_LANE_RECORD_FIELDS, LANE_FIELD_WRITERS, LANE_WRITERS, } from "./types.js";
|
|
17
|
+
import { laneStories } from "./stories.js";
|
|
17
18
|
export const LANES_STORE_REL = path.join("workspace", "lanes");
|
|
18
19
|
export const LANE_RECORDS_DIR = "lanes";
|
|
19
20
|
export const QUESTION_RECORDS_DIR = "questions";
|
|
@@ -277,6 +278,80 @@ function parseOptionalMeshCompanyField(raw, filePath) {
|
|
|
277
278
|
requireString(raw.mesh_company.slug, "mesh_company.slug");
|
|
278
279
|
requireString(raw.mesh_company.uid, "mesh_company.uid");
|
|
279
280
|
}
|
|
281
|
+
function legacyLaneStory(raw) {
|
|
282
|
+
const status = raw.state === "blocked"
|
|
283
|
+
? "blocked"
|
|
284
|
+
: raw.story_status === "review"
|
|
285
|
+
? "review"
|
|
286
|
+
: raw.story_status === "done" || raw.story_complete === true
|
|
287
|
+
? "review"
|
|
288
|
+
: "in_progress";
|
|
289
|
+
const story = {
|
|
290
|
+
project_id: requireString(raw.project_id, "project_id"),
|
|
291
|
+
story_id: requireString(raw.story_id, "story_id"),
|
|
292
|
+
status,
|
|
293
|
+
added_at: requireString(raw.created_at, "created_at"),
|
|
294
|
+
updated_at: requireString(raw.updated_at, "updated_at"),
|
|
295
|
+
};
|
|
296
|
+
if (typeof raw.blocked_reason === "string" && raw.blocked_reason.length > 0) {
|
|
297
|
+
story.blocked_reason = raw.blocked_reason;
|
|
298
|
+
}
|
|
299
|
+
return story;
|
|
300
|
+
}
|
|
301
|
+
function parseLaneStories(raw, filePath) {
|
|
302
|
+
if (raw.stories === undefined) {
|
|
303
|
+
// v1 records carried one story in the top-level fields. Keep the old
|
|
304
|
+
// fields authoritative for the migration and give worker-completed work
|
|
305
|
+
// the review state until a senior explicitly verifies it.
|
|
306
|
+
raw.stories = [legacyLaneStory(raw)];
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (!Array.isArray(raw.stories) || raw.stories.length === 0) {
|
|
310
|
+
throw new LaneSchemaError("stories must be a non-empty array when present", {
|
|
311
|
+
path: filePath,
|
|
312
|
+
field: "stories",
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
for (const [index, value] of raw.stories.entries()) {
|
|
316
|
+
const field = `stories[${index}]`;
|
|
317
|
+
if (!isPlainObject(value)) {
|
|
318
|
+
throw new LaneSchemaError(`${field} must be an object`, { path: filePath, field });
|
|
319
|
+
}
|
|
320
|
+
requireString(value.project_id, `${field}.project_id`);
|
|
321
|
+
requireString(value.story_id, `${field}.story_id`);
|
|
322
|
+
requireString(value.added_at, `${field}.added_at`);
|
|
323
|
+
requireString(value.updated_at, `${field}.updated_at`);
|
|
324
|
+
if (typeof value.status !== "string" ||
|
|
325
|
+
!LANE_STORY_STATUSES.includes(value.status)) {
|
|
326
|
+
throw new LaneSchemaError(`Unknown story status ${JSON.stringify(value.status)}`, {
|
|
327
|
+
path: filePath,
|
|
328
|
+
field: `${field}.status`,
|
|
329
|
+
value: value.status,
|
|
330
|
+
accepted: [...LANE_STORY_STATUSES],
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
if (value.blocked_reason !== undefined) {
|
|
334
|
+
requireString(value.blocked_reason, `${field}.blocked_reason`);
|
|
335
|
+
}
|
|
336
|
+
if (value.verified_at !== undefined) {
|
|
337
|
+
requireString(value.verified_at, `${field}.verified_at`);
|
|
338
|
+
}
|
|
339
|
+
if (value.verified_by !== undefined) {
|
|
340
|
+
if (!isPlainObject(value.verified_by)) {
|
|
341
|
+
throw new LaneSchemaError(`${field}.verified_by must be an object`, {
|
|
342
|
+
path: filePath,
|
|
343
|
+
field: `${field}.verified_by`,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
if (!isSeniorKind(value.verified_by.kind)) {
|
|
347
|
+
throw new LaneSchemaError(`Unknown senior kind ${JSON.stringify(value.verified_by.kind)}`, { path: filePath, field: `${field}.verified_by.kind` });
|
|
348
|
+
}
|
|
349
|
+
if (value.verified_by.kind !== "user") {
|
|
350
|
+
requireString(value.verified_by.id, `${field}.verified_by.id`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
280
355
|
export function parseLaneRecord(raw, filePath) {
|
|
281
356
|
if (!isPlainObject(raw)) {
|
|
282
357
|
throw new LaneSchemaError("Lane record must be a JSON object", {
|
|
@@ -302,6 +377,7 @@ export function parseLaneRecord(raw, filePath) {
|
|
|
302
377
|
if (!isLaneProvider(raw.provider)) {
|
|
303
378
|
throw new LaneSchemaError(`Unknown provider ${JSON.stringify(raw.provider)}`, { path: filePath, value: raw.provider });
|
|
304
379
|
}
|
|
380
|
+
parseLaneStories(raw, filePath);
|
|
305
381
|
parseOptionalCompanyField(raw, filePath);
|
|
306
382
|
parseOptionalMeshCompanyField(raw, filePath);
|
|
307
383
|
if (!isPlainObject(raw.senior)) {
|
|
@@ -472,6 +548,37 @@ function higherVersionRecord(live, others) {
|
|
|
472
548
|
}
|
|
473
549
|
return best;
|
|
474
550
|
}
|
|
551
|
+
function laneStoryIdentity(story) {
|
|
552
|
+
return `${story.project_id}\u0000${story.story_id}`;
|
|
553
|
+
}
|
|
554
|
+
function mergeLaneStories(records, base) {
|
|
555
|
+
const baseStories = laneStories(base);
|
|
556
|
+
const merged = new Map();
|
|
557
|
+
const baseOrder = [];
|
|
558
|
+
for (const story of baseStories) {
|
|
559
|
+
const key = laneStoryIdentity(story);
|
|
560
|
+
if (!merged.has(key))
|
|
561
|
+
baseOrder.push(key);
|
|
562
|
+
merged.set(key, story);
|
|
563
|
+
}
|
|
564
|
+
for (const record of records) {
|
|
565
|
+
if (record === base)
|
|
566
|
+
continue;
|
|
567
|
+
for (const story of laneStories(record)) {
|
|
568
|
+
const key = laneStoryIdentity(story);
|
|
569
|
+
const current = merged.get(key);
|
|
570
|
+
if (!current || story.updated_at > current.updated_at) {
|
|
571
|
+
merged.set(key, story);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const sideOnly = [...merged.keys()]
|
|
576
|
+
.filter((key) => !baseOrder.includes(key))
|
|
577
|
+
.sort();
|
|
578
|
+
return [...baseOrder, ...sideOnly]
|
|
579
|
+
.map((key) => merged.get(key))
|
|
580
|
+
.filter((story) => story !== undefined);
|
|
581
|
+
}
|
|
475
582
|
export function mergeLaneRecords(live, sidecars) {
|
|
476
583
|
if (sidecars.length === 0)
|
|
477
584
|
return live;
|
|
@@ -486,6 +593,7 @@ export function mergeLaneRecords(live, sidecars) {
|
|
|
486
593
|
account_history: unionHistory(all.map((record) => record.account_history ?? []), entryIdentity, base.account_history ?? []),
|
|
487
594
|
}
|
|
488
595
|
: {}),
|
|
596
|
+
stories: mergeLaneStories(all, base),
|
|
489
597
|
};
|
|
490
598
|
}
|
|
491
599
|
export function mergeQuestionRecords(live, sidecars) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { LaneRecord, LaneStory } from "./types.js";
|
|
2
|
+
/** Board-facing states for the stories a lane currently carries. */
|
|
3
|
+
export declare const ACTIVE_LANE_STORY_STATUSES: readonly ["in_progress", "blocked", "review"];
|
|
4
|
+
export declare function storyKey(projectId: string, storyId: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Read the v1 multi-story field while keeping old one-story records usable.
|
|
7
|
+
* The returned legacy row is deliberately in-memory until the next normal
|
|
8
|
+
* lane write, so reading an old record never changes it on disk by itself.
|
|
9
|
+
*/
|
|
10
|
+
export declare function laneStories(lane: LaneRecord): LaneStory[];
|
|
11
|
+
export declare function activeLaneStories(lane: LaneRecord): LaneStory[];
|
|
12
|
+
export declare function laneStory(lane: LaneRecord, projectId: string, storyId: string): LaneStory | undefined;
|
|
13
|
+
export declare function storyWarning(lane: LaneRecord): string | undefined;
|
|
14
|
+
export declare function withCurrentStory(lane: LaneRecord, projectId: string, storyId: string): LaneRecord;
|
|
15
|
+
export declare function withLaneStories(lane: LaneRecord, stories: LaneStory[]): LaneRecord;
|
|
16
|
+
export declare function storyStatusForEnvelope(status: string): "in_progress" | "review";
|
|
17
|
+
//# sourceMappingURL=stories.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Board-facing states for the stories a lane currently carries. */
|
|
2
|
+
export const ACTIVE_LANE_STORY_STATUSES = [
|
|
3
|
+
"in_progress",
|
|
4
|
+
"blocked",
|
|
5
|
+
"review",
|
|
6
|
+
];
|
|
7
|
+
export function storyKey(projectId, storyId) {
|
|
8
|
+
return `${projectId}\u0000${storyId}`;
|
|
9
|
+
}
|
|
10
|
+
function legacyStatus(lane) {
|
|
11
|
+
if (lane.state === "blocked")
|
|
12
|
+
return "blocked";
|
|
13
|
+
if (lane.story_status === "review")
|
|
14
|
+
return "review";
|
|
15
|
+
// A worker envelope was historically allowed to say done. The worker's
|
|
16
|
+
// terminal result only earns review; senior verification is the sole path
|
|
17
|
+
// to the durable done state.
|
|
18
|
+
if (lane.story_status === "done" || lane.story_complete === true) {
|
|
19
|
+
return "review";
|
|
20
|
+
}
|
|
21
|
+
return "in_progress";
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Read the v1 multi-story field while keeping old one-story records usable.
|
|
25
|
+
* The returned legacy row is deliberately in-memory until the next normal
|
|
26
|
+
* lane write, so reading an old record never changes it on disk by itself.
|
|
27
|
+
*/
|
|
28
|
+
export function laneStories(lane) {
|
|
29
|
+
if (Array.isArray(lane.stories) && lane.stories.length > 0) {
|
|
30
|
+
return lane.stories;
|
|
31
|
+
}
|
|
32
|
+
const at = lane.updated_at || lane.created_at;
|
|
33
|
+
return [
|
|
34
|
+
{
|
|
35
|
+
project_id: lane.project_id,
|
|
36
|
+
story_id: lane.story_id,
|
|
37
|
+
status: legacyStatus(lane),
|
|
38
|
+
added_at: lane.created_at,
|
|
39
|
+
updated_at: at,
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
export function activeLaneStories(lane) {
|
|
44
|
+
return laneStories(lane).filter((story) => ACTIVE_LANE_STORY_STATUSES.includes(story.status));
|
|
45
|
+
}
|
|
46
|
+
export function laneStory(lane, projectId, storyId) {
|
|
47
|
+
const key = storyKey(projectId, storyId);
|
|
48
|
+
return laneStories(lane).find((story) => storyKey(story.project_id, story.story_id) === key);
|
|
49
|
+
}
|
|
50
|
+
export function storyWarning(lane) {
|
|
51
|
+
const count = activeLaneStories(lane).length;
|
|
52
|
+
if (count <= 1)
|
|
53
|
+
return undefined;
|
|
54
|
+
return `Lane ${lane.lane_id} carries ${count} active stories; parallel story work is allowed but discouraged.`;
|
|
55
|
+
}
|
|
56
|
+
export function withCurrentStory(lane, projectId, storyId) {
|
|
57
|
+
return {
|
|
58
|
+
...lane,
|
|
59
|
+
project_id: projectId,
|
|
60
|
+
story_id: storyId,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function withLaneStories(lane, stories) {
|
|
64
|
+
return { ...lane, stories };
|
|
65
|
+
}
|
|
66
|
+
export function storyStatusForEnvelope(status) {
|
|
67
|
+
// A worker's `done` envelope is reviewable work. Durable `done` is reserved
|
|
68
|
+
// for the senior verification command.
|
|
69
|
+
if (status === "review")
|
|
70
|
+
return "review";
|
|
71
|
+
return "in_progress";
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=stories.js.map
|
|
@@ -176,6 +176,9 @@ export declare const ENVELOPE_DECISIONS: readonly ["done", "ask", "blocked"];
|
|
|
176
176
|
export type EnvelopeDecision = (typeof ENVELOPE_DECISIONS)[number];
|
|
177
177
|
export declare const STORY_STATUSES: readonly ["queued", "in_progress", "review", "done"];
|
|
178
178
|
export type StoryStatus = (typeof STORY_STATUSES)[number];
|
|
179
|
+
/** Durable Board status for a story carried by a lane. */
|
|
180
|
+
export declare const LANE_STORY_STATUSES: readonly ["in_progress", "blocked", "review", "done"];
|
|
181
|
+
export type LaneStoryStatus = (typeof LANE_STORY_STATUSES)[number];
|
|
179
182
|
/**
|
|
180
183
|
* `expired` is accepted on read for records that already carry it. Spec §9
|
|
181
184
|
* ageing advances hop with `unanswered_timeout` and never expires a question,
|
|
@@ -237,6 +240,16 @@ export interface LastEnvelopeRef {
|
|
|
237
240
|
decision: EnvelopeDecision;
|
|
238
241
|
at: string;
|
|
239
242
|
}
|
|
243
|
+
export interface LaneStory {
|
|
244
|
+
project_id: string;
|
|
245
|
+
story_id: string;
|
|
246
|
+
status: LaneStoryStatus;
|
|
247
|
+
added_at: string;
|
|
248
|
+
updated_at: string;
|
|
249
|
+
blocked_reason?: string;
|
|
250
|
+
verified_at?: string;
|
|
251
|
+
verified_by?: SeniorRef;
|
|
252
|
+
}
|
|
240
253
|
export interface LaneRecord {
|
|
241
254
|
schema_version: typeof LANE_SCHEMA_VERSION | string;
|
|
242
255
|
lane_id: string;
|
|
@@ -246,6 +259,11 @@ export interface LaneRecord {
|
|
|
246
259
|
mesh_company?: LaneCompanyRef;
|
|
247
260
|
project_id: string;
|
|
248
261
|
story_id: string;
|
|
262
|
+
/**
|
|
263
|
+
* Stories currently carried by this lane. Old records omit this field and
|
|
264
|
+
* are migrated in memory from project_id/story_id on read.
|
|
265
|
+
*/
|
|
266
|
+
stories?: LaneStory[];
|
|
249
267
|
thread_id?: string;
|
|
250
268
|
depth: number;
|
|
251
269
|
senior: SeniorRef;
|
|
@@ -367,6 +385,8 @@ export interface LaneRecord {
|
|
|
367
385
|
*/
|
|
368
386
|
provider_session_id?: string;
|
|
369
387
|
provider_session_id_absent?: string;
|
|
388
|
+
/** Provider home used by the most recently spawned run, for transcript recovery. */
|
|
389
|
+
provider_home_at_spawn?: string;
|
|
370
390
|
/** Set at spawn when this run continued a recorded session instead of starting fresh. */
|
|
371
391
|
last_resume?: LaneResumeRecord;
|
|
372
392
|
/** Cache counters from the last exited run's result. Nothing else from the payload. */
|
package/dist/lib/lanes/types.js
CHANGED
|
@@ -105,6 +105,13 @@ export const STORY_STATUSES = [
|
|
|
105
105
|
"review",
|
|
106
106
|
"done",
|
|
107
107
|
];
|
|
108
|
+
/** Durable Board status for a story carried by a lane. */
|
|
109
|
+
export const LANE_STORY_STATUSES = [
|
|
110
|
+
"in_progress",
|
|
111
|
+
"blocked",
|
|
112
|
+
"review",
|
|
113
|
+
"done",
|
|
114
|
+
];
|
|
108
115
|
/**
|
|
109
116
|
* `expired` is accepted on read for records that already carry it. Spec §9
|
|
110
117
|
* ageing advances hop with `unanswered_timeout` and never expires a question,
|
|
@@ -195,7 +202,7 @@ export const LANE_WRITERS = ["creator", "command", "watcher", "reconcile", "spaw
|
|
|
195
202
|
/** Field ownership at the durable-record mutation boundary (spec §10.1). */
|
|
196
203
|
export const LANE_FIELD_WRITERS = {
|
|
197
204
|
schema_version: ["creator"], lane_id: ["creator"], company: ["creator"],
|
|
198
|
-
project_id: ["command"], story_id: ["command"], thread_id: ["creator", "command"], mesh_company: ["creator"],
|
|
205
|
+
project_id: ["command"], story_id: ["command"], stories: ["creator", "command", "watcher", "reconcile", "spawn"], thread_id: ["creator", "command", "spawn"], mesh_company: ["creator"],
|
|
199
206
|
depth: ["command"], senior: ["command"], senior_history: ["command"],
|
|
200
207
|
state: ["creator", "command", "watcher", "reconcile", "spawn"], queued_reason: ["creator", "command", "spawn", "reconcile"],
|
|
201
208
|
pickup_validation_pending: ["creator", "command", "spawn", "reconcile"],
|
|
@@ -208,7 +215,7 @@ export const LANE_FIELD_WRITERS = {
|
|
|
208
215
|
overdue: ["reconcile"], overdue_at: ["reconcile"], last_host_report_at: ["watcher", "spawn", "reconcile"], stalled: ["watcher"], stalled_at: ["watcher"], stalled_reason: ["watcher"],
|
|
209
216
|
admitted_reason: ["reconcile"], terminal_reason: ["command", "watcher", "spawn", "reconcile"], terminal_details: ["command", "spawn", "watcher", "reconcile"],
|
|
210
217
|
quota_reset_at: ["watcher", "reconcile"], model_quota_marker: ["watcher", "reconcile"], outpost_command_id: ["command"],
|
|
211
|
-
provider_session_id: ["watcher"], provider_session_id_absent: ["watcher"], last_resume: ["spawn"], last_run_usage: ["watcher"], envelope_repair: ["watcher", "reconcile"], last_progress_event_at: ["watcher"],
|
|
218
|
+
provider_session_id: ["watcher", "command", "spawn"], provider_session_id_absent: ["watcher", "command"], provider_home_at_spawn: ["spawn"], last_resume: ["spawn"], last_run_usage: ["watcher"], envelope_repair: ["watcher", "reconcile"], last_progress_event_at: ["watcher"],
|
|
212
219
|
};
|
|
213
220
|
function isPlainObject(value) {
|
|
214
221
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -36,6 +36,7 @@ import * as path from "node:path";
|
|
|
36
36
|
import { LanesError } from "./errors.js";
|
|
37
37
|
import { raiseEnvelopeQuestions } from "./questions.js";
|
|
38
38
|
import { isTerminalLaneState, parseLaneEnvelope, } from "./types.js";
|
|
39
|
+
import { laneStories, storyStatusForEnvelope } from "./stories.js";
|
|
39
40
|
import { readLane, retryUpdateLane } from "./store.js";
|
|
40
41
|
import { readWorkerRunResult } from "./delivery.js";
|
|
41
42
|
import { applyDefaultQuotaHold, quotaSignalFromFields, readQuotaSignal, recordAccountQuota, } from "./quota.js";
|
|
@@ -243,8 +244,15 @@ function runResultFields(hqRoot, lane) {
|
|
|
243
244
|
fields.provider_session_id_absent = undefined;
|
|
244
245
|
}
|
|
245
246
|
else {
|
|
246
|
-
|
|
247
|
-
|
|
247
|
+
// A resumed provider may exit before it emits a new result object. Keep
|
|
248
|
+
// the prior session id so the lane remains resumable; only describe an
|
|
249
|
+
// absent id when no earlier run recorded one.
|
|
250
|
+
if (lane.provider_session_id || lane.last_resume?.session_id) {
|
|
251
|
+
fields.provider_session_id_absent = undefined;
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
fields.provider_session_id_absent = result.session_id_absent ?? "no_result_json";
|
|
255
|
+
}
|
|
248
256
|
}
|
|
249
257
|
return fields;
|
|
250
258
|
}
|
|
@@ -337,17 +345,36 @@ export function applyTerminalToLaneWithResult(hqRoot, laneId, classification, op
|
|
|
337
345
|
if (opts.providerRejection) {
|
|
338
346
|
next.terminal_details = providerRejectionDetails(opts.providerRejection, current);
|
|
339
347
|
}
|
|
340
|
-
|
|
348
|
+
const envelope = opts.envelope;
|
|
349
|
+
if (envelope && opts.envelopeRef) {
|
|
341
350
|
next.last_envelope = {
|
|
342
351
|
ref: opts.envelopeRef,
|
|
343
|
-
decision:
|
|
352
|
+
decision: envelope.decision,
|
|
344
353
|
at,
|
|
345
354
|
};
|
|
346
|
-
next.story_status =
|
|
347
|
-
if (
|
|
348
|
-
next.story_complete =
|
|
349
|
-
next.story_complete_reason =
|
|
355
|
+
next.story_status = envelope.story_status;
|
|
356
|
+
if (envelope.story_complete !== undefined) {
|
|
357
|
+
next.story_complete = envelope.story_complete;
|
|
358
|
+
next.story_complete_reason = envelope.story_complete_reason;
|
|
350
359
|
}
|
|
360
|
+
const currentProject = current.project_id;
|
|
361
|
+
const currentStory = current.story_id;
|
|
362
|
+
const storyStatus = envelope.decision === "blocked"
|
|
363
|
+
? "blocked"
|
|
364
|
+
: envelope.decision === "done"
|
|
365
|
+
? "review"
|
|
366
|
+
: storyStatusForEnvelope(envelope.story_status);
|
|
367
|
+
const updatedStories = laneStories(current).map((story) => story.project_id === currentProject && story.story_id === currentStory
|
|
368
|
+
? {
|
|
369
|
+
...story,
|
|
370
|
+
status: storyStatus,
|
|
371
|
+
updated_at: at,
|
|
372
|
+
...(envelope.decision === "blocked"
|
|
373
|
+
? { blocked_reason: envelope.blocked_reason ?? "" }
|
|
374
|
+
: { blocked_reason: undefined }),
|
|
375
|
+
}
|
|
376
|
+
: story);
|
|
377
|
+
next.stories = updatedStories;
|
|
351
378
|
}
|
|
352
379
|
return next;
|
|
353
380
|
});
|
|
@@ -550,9 +577,42 @@ export async function runWatcherLoop(hqRoot, laneId, deps = {}) {
|
|
|
550
577
|
const progressFiles = [...logFiles, ...activityFiles];
|
|
551
578
|
// Mesh seam (spec §8). Non-interactive: nobody is at this process's tty.
|
|
552
579
|
const mesh = deps.mesh === undefined ? createLaneMesh({ interactive: false }) : deps.mesh;
|
|
580
|
+
const meshRequestTimeoutMs = deps.meshRequestTimeoutMs ?? 10_000;
|
|
581
|
+
const initialLane = readLaneSoft(hqRoot, laneId);
|
|
582
|
+
if (mesh && initialLane) {
|
|
583
|
+
// Explicit mesh attribution is the repair signal. Ordinary company lanes
|
|
584
|
+
// already receive their thread during create; legacy records without one
|
|
585
|
+
// stay quiet until an operator or the owning flow repairs them.
|
|
586
|
+
const company = initialLane.mesh_company;
|
|
587
|
+
if (company && !initialLane.thread_id) {
|
|
588
|
+
let timer;
|
|
589
|
+
try {
|
|
590
|
+
const threadId = await Promise.race([
|
|
591
|
+
mesh.ensureProjectThread(company, initialLane.project_id),
|
|
592
|
+
new Promise((_resolve, reject) => {
|
|
593
|
+
timer = setTimeout(() => reject(new Error(`mesh thread ensure timed out after ${meshRequestTimeoutMs}ms`)), meshRequestTimeoutMs);
|
|
594
|
+
}),
|
|
595
|
+
]);
|
|
596
|
+
retryUpdateLane(hqRoot, laneId, "spawn", (current) => current.thread_id ? current : { ...current, thread_id: threadId });
|
|
597
|
+
}
|
|
598
|
+
catch (err) {
|
|
599
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
600
|
+
process.stderr.write(`hq lanes watcher: mesh thread ensure failed for ${laneId}: ${message}\n`);
|
|
601
|
+
writeWatcherDecision(outPath, {
|
|
602
|
+
event: "mesh_event_failed",
|
|
603
|
+
at: new Date(now()).toISOString(),
|
|
604
|
+
mesh_event: "ensure_project_thread",
|
|
605
|
+
error: message,
|
|
606
|
+
}, outMax);
|
|
607
|
+
}
|
|
608
|
+
finally {
|
|
609
|
+
if (timer)
|
|
610
|
+
clearTimeout(timer);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
553
614
|
const progressIntervalMs = deps.progressEventMinIntervalMs ?? readProgressIntervalMs(hqRoot, readLaneSoft(hqRoot, laneId));
|
|
554
615
|
const spawnRepair = deps.spawnRepair ?? spawnEnvelopeRepair;
|
|
555
|
-
const meshRequestTimeoutMs = deps.meshRequestTimeoutMs ?? 10_000;
|
|
556
616
|
let lastSignalMs = now();
|
|
557
617
|
let lastSeenMtime = 0;
|
|
558
618
|
let lastAlive = null;
|
|
@@ -57,6 +57,15 @@ export interface DaemonDoctorReport {
|
|
|
57
57
|
deviceDefaultAuthorizationDenials: number;
|
|
58
58
|
/** True when the pack-era listen unit is still on disk. */
|
|
59
59
|
legacyListenerInstalled: boolean;
|
|
60
|
+
/** One line when the running daemon's recorded CLI is not this CLI. */
|
|
61
|
+
cliMismatchLine?: string;
|
|
62
|
+
/** Every `hq` executable found on PATH, with the package version behind it. */
|
|
63
|
+
hqOnPath: Array<{
|
|
64
|
+
path: string;
|
|
65
|
+
version: string | null;
|
|
66
|
+
}>;
|
|
67
|
+
/** True when PATH lists more than one hq. */
|
|
68
|
+
multipleHqOnPath: boolean;
|
|
60
69
|
}
|
|
61
70
|
export interface DaemonDoctorDeps {
|
|
62
71
|
home?: string;
|
|
@@ -70,7 +79,25 @@ export interface DaemonDoctorDeps {
|
|
|
70
79
|
/** Override the legacy-listen probe (tests). */
|
|
71
80
|
legacyListenerInstalled?: boolean;
|
|
72
81
|
platform?: LegacyListenPlatform;
|
|
82
|
+
/** This process's CLI version. Defaults to the package version. */
|
|
83
|
+
cliVersion?: string;
|
|
84
|
+
/** This process's CLI entry. Defaults to realpath of argv[1]. */
|
|
85
|
+
cliEntry?: string;
|
|
86
|
+
/** PATH used to list hq binaries. Defaults to the process PATH. */
|
|
87
|
+
pathEnv?: string;
|
|
73
88
|
}
|
|
89
|
+
/** Every hq named on PATH. Does not pick one; the caller prints all of them. */
|
|
90
|
+
export declare function listHqBinariesOnPath(env?: NodeJS.ProcessEnv): Array<{
|
|
91
|
+
path: string;
|
|
92
|
+
version: string | null;
|
|
93
|
+
}>;
|
|
94
|
+
export declare function formatCliMismatchLine(input: {
|
|
95
|
+
recordedVersion?: string | null;
|
|
96
|
+
recordedEntry?: string | null;
|
|
97
|
+
currentVersion: string;
|
|
98
|
+
currentEntry: string;
|
|
99
|
+
}): string | undefined;
|
|
100
|
+
export declare function formatCliIdentityLines(report: Pick<DaemonDoctorReport, "cliMismatchLine" | "hqOnPath" | "multipleHqOnPath">): string[];
|
|
74
101
|
export declare function collectDaemonDoctor(deps?: DaemonDoctorDeps): DaemonDoctorReport;
|
|
75
102
|
export declare function formatDaemonDoctor(report: DaemonDoctorReport): string[];
|
|
76
103
|
//# sourceMappingURL=doctor.d.ts.map
|
|
@@ -19,6 +19,9 @@ import { hasCurrentAcknowledgedRegistration, readHeldSessions, refWithLocalCompa
|
|
|
19
19
|
import { readDaemonConfig } from "./config.js";
|
|
20
20
|
import { pidLockStatus, readPidLock } from "./pid-lock.js";
|
|
21
21
|
import { readDaemonState } from "./state.js";
|
|
22
|
+
import { CLI_VERSION } from "../../../../cli-version.js";
|
|
23
|
+
import { resolveRunningCliEntry } from "./install.js";
|
|
24
|
+
import { readInstalledCliVersion } from "./version-drift.js";
|
|
22
25
|
import { LEGACY_LISTENER_DOCTOR_LINE, legacyListenInstalled, } from "./legacy-listen.js";
|
|
23
26
|
export const UNHEALTHY_SPOOL_AGE_MS = 60_000;
|
|
24
27
|
function oldestJsonlAgeMs(filePath, nowMs) {
|
|
@@ -55,6 +58,65 @@ function oldestJsonlAgeMs(filePath, nowMs) {
|
|
|
55
58
|
return undefined;
|
|
56
59
|
}
|
|
57
60
|
}
|
|
61
|
+
function samePath(a, b) {
|
|
62
|
+
const left = tryRealpath(a);
|
|
63
|
+
const right = tryRealpath(b);
|
|
64
|
+
return left === right;
|
|
65
|
+
}
|
|
66
|
+
function tryRealpath(value) {
|
|
67
|
+
try {
|
|
68
|
+
return fs.realpathSync(value);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return path.resolve(value);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Every hq named on PATH. Does not pick one; the caller prints all of them. */
|
|
75
|
+
export function listHqBinariesOnPath(env = process.env) {
|
|
76
|
+
const found = [];
|
|
77
|
+
const pathEnv = env.PATH ?? "";
|
|
78
|
+
const names = process.platform === "win32" ? ["hq.cmd", "hq.exe", "hq"] : ["hq"];
|
|
79
|
+
for (const dir of pathEnv.split(path.delimiter)) {
|
|
80
|
+
if (!dir)
|
|
81
|
+
continue;
|
|
82
|
+
for (const name of names) {
|
|
83
|
+
const candidate = path.join(dir, name);
|
|
84
|
+
if (!fs.existsSync(candidate))
|
|
85
|
+
continue;
|
|
86
|
+
found.push({
|
|
87
|
+
path: tryRealpath(candidate),
|
|
88
|
+
version: readInstalledCliVersion(candidate),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return found;
|
|
93
|
+
}
|
|
94
|
+
export function formatCliMismatchLine(input) {
|
|
95
|
+
const recordedVersion = input.recordedVersion?.trim() || "";
|
|
96
|
+
const recordedEntry = input.recordedEntry?.trim() || "";
|
|
97
|
+
const versionMatches = recordedVersion !== "" && recordedVersion === input.currentVersion;
|
|
98
|
+
const entryMatches = recordedEntry !== "" &&
|
|
99
|
+
input.currentEntry !== "" &&
|
|
100
|
+
samePath(recordedEntry, input.currentEntry);
|
|
101
|
+
if (versionMatches && entryMatches)
|
|
102
|
+
return undefined;
|
|
103
|
+
const recordedLabel = recordedVersion || "(none)";
|
|
104
|
+
const recordedAt = recordedEntry || "(none)";
|
|
105
|
+
const currentAt = input.currentEntry || "(none)";
|
|
106
|
+
return (`daemon hq ${recordedLabel} at ${recordedAt} does not match this CLI ` +
|
|
107
|
+
`${input.currentVersion} at ${currentAt}. Run hq mesh daemon install to repoint it.`);
|
|
108
|
+
}
|
|
109
|
+
export function formatCliIdentityLines(report) {
|
|
110
|
+
const lines = [];
|
|
111
|
+
if (report.cliMismatchLine)
|
|
112
|
+
lines.push(report.cliMismatchLine);
|
|
113
|
+
for (const hit of report.hqOnPath) {
|
|
114
|
+
lines.push(`hq on PATH: ${hit.path} (${hit.version ?? "unknown"})`);
|
|
115
|
+
}
|
|
116
|
+
if (report.multipleHqOnPath)
|
|
117
|
+
lines.push("more than one hq is on PATH");
|
|
118
|
+
return lines;
|
|
119
|
+
}
|
|
58
120
|
function countDeviceDefaultAttribution(root) {
|
|
59
121
|
try {
|
|
60
122
|
return fs.readdirSync(path.join(root, "sessions")).filter((entry) => {
|
|
@@ -93,6 +155,24 @@ export function collectDaemonDoctor(deps = {}) {
|
|
|
93
155
|
const record = readPidLock(lockPath);
|
|
94
156
|
const running = lock === "held";
|
|
95
157
|
const state = readDaemonState(dir);
|
|
158
|
+
let currentEntry = deps.cliEntry ?? "";
|
|
159
|
+
if (!deps.cliEntry) {
|
|
160
|
+
try {
|
|
161
|
+
currentEntry = resolveRunningCliEntry();
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
currentEntry = "";
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const currentVersion = deps.cliVersion ?? CLI_VERSION;
|
|
168
|
+
const pathEnv = deps.pathEnv ?? env.PATH ?? "";
|
|
169
|
+
const hqOnPath = listHqBinariesOnPath({ ...env, PATH: pathEnv });
|
|
170
|
+
const cliMismatchLine = formatCliMismatchLine({
|
|
171
|
+
recordedVersion: state?.cliVersion,
|
|
172
|
+
recordedEntry: state?.cliEntry,
|
|
173
|
+
currentVersion,
|
|
174
|
+
currentEntry,
|
|
175
|
+
});
|
|
96
176
|
const spoolPath = workMeshSpoolPath(meshRoot);
|
|
97
177
|
const heldPath = workMeshHeldPath(meshRoot);
|
|
98
178
|
const deadPath = workMeshDeadLetterPath(meshRoot);
|
|
@@ -203,6 +283,9 @@ export function collectDaemonDoctor(deps = {}) {
|
|
|
203
283
|
: undefined,
|
|
204
284
|
deviceDefaultAuthorizationDenials,
|
|
205
285
|
legacyListenerInstalled: legacyListener,
|
|
286
|
+
cliMismatchLine,
|
|
287
|
+
hqOnPath,
|
|
288
|
+
multipleHqOnPath: hqOnPath.length > 1,
|
|
206
289
|
};
|
|
207
290
|
}
|
|
208
291
|
export function formatDaemonDoctor(report) {
|
|
@@ -253,6 +336,7 @@ export function formatDaemonDoctor(report) {
|
|
|
253
336
|
else {
|
|
254
337
|
lines.push(`health: ok`);
|
|
255
338
|
}
|
|
339
|
+
lines.push(...formatCliIdentityLines(report));
|
|
256
340
|
return lines;
|
|
257
341
|
}
|
|
258
342
|
//# sourceMappingURL=doctor.js.map
|
|
@@ -12,12 +12,12 @@ export type { DaemonStateFile, PresenceRefusalState } from "./state.js";
|
|
|
12
12
|
export { appendDaemonLog, daemonLogLine, ensureDaemonLog, resolveDaemonAssetDir, rotateDaemonLogIfNeeded, } from "./log.js";
|
|
13
13
|
export { BOARD_REFRESH_INTERVAL_MS, createVaultBoardReader, formatBoardMarkdown, refreshBoundSessionBoards, writeBoardMarkdown, } from "./board-refresh.js";
|
|
14
14
|
export type { BoardReader, BoardRefreshDeps, BoardStorySnapshot } from "./board-refresh.js";
|
|
15
|
-
export { buildInstallPaths, daemonServiceStatus, detectPlatform, installDaemonService, readAssetTemplate, restartDaemonServiceIfInstalled, renderLaunchdPlist, renderSystemdSystemUnit, renderSystemdUserUnit, resolveHqBinary, resolveNodeBinary, parseSchtasksLastResult, parseWindowsTaskHqRoot, readRecordedWindowsHqRoot, schtasksLastResultFailed, uninstallDaemonService, windowsLaunchMode, windowsLogonNotice, } from "./install.js";
|
|
16
|
-
export type { DaemonInstallPlatform, InstallPaths, InstallResult, } from "./install.js";
|
|
15
|
+
export { buildInstallPaths, daemonServiceStatus, detectPlatform, installDaemonIfStale, installDaemonService, isNewerCliVersion, readAssetTemplate, restartDaemonServiceIfInstalled, renderLaunchdPlist, renderSystemdSystemUnit, renderSystemdUserUnit, resolveAbsoluteNodeBinary, resolveHqBinary, resolveNodeBinary, resolveRunningCliEntry, parseSchtasksLastResult, parseWindowsTaskHqRoot, readRecordedWindowsHqRoot, schtasksLastResultFailed, uninstallDaemonService, windowsLaunchMode, windowsLogonNotice, } from "./install.js";
|
|
16
|
+
export type { DaemonInstallPlatform, InstallIfStaleResult, InstallPaths, InstallResult, } from "./install.js";
|
|
17
17
|
export { daemonConfigPath, findAncestorHqRoot, isInstallableHqRoot, readDaemonConfig, registerTrustedHqRoot, resolveInstallHqRoot, verifyHqRoot, } from "./config.js";
|
|
18
18
|
export type { InstallHqRootResolution } from "./config.js";
|
|
19
19
|
export type { DaemonConfig } from "./config.js";
|
|
20
|
-
export { UNHEALTHY_SPOOL_AGE_MS, collectDaemonDoctor, formatDaemonDoctor, } from "./doctor.js";
|
|
20
|
+
export { UNHEALTHY_SPOOL_AGE_MS, collectDaemonDoctor, formatCliIdentityLines, formatCliMismatchLine, formatDaemonDoctor, listHqBinariesOnPath, } from "./doctor.js";
|
|
21
21
|
export type { DaemonDoctorDeps, DaemonDoctorReport } from "./doctor.js";
|
|
22
22
|
export { scopeBundleToAgentIdentity, } from "./credentials.js";
|
|
23
23
|
export { FLUSH_INTERVAL_MS, DAEMON_VERSION_DRIFT_EXIT_CODE, SPOOL_DEBOUNCE_MS, runMeshDaemon, } from "./run.js";
|
|
@@ -6,9 +6,9 @@ export { CLOSE_LOG_ALWAYS_COUNT, CLOSE_LOG_INTERVAL_MS, CONNECT_STABLE_GRACE_MS,
|
|
|
6
6
|
export { defaultDaemonState, patchDaemonState, readDaemonState, writeDaemonState, } from "./state.js";
|
|
7
7
|
export { appendDaemonLog, daemonLogLine, ensureDaemonLog, resolveDaemonAssetDir, rotateDaemonLogIfNeeded, } from "./log.js";
|
|
8
8
|
export { BOARD_REFRESH_INTERVAL_MS, createVaultBoardReader, formatBoardMarkdown, refreshBoundSessionBoards, writeBoardMarkdown, } from "./board-refresh.js";
|
|
9
|
-
export { buildInstallPaths, daemonServiceStatus, detectPlatform, installDaemonService, readAssetTemplate, restartDaemonServiceIfInstalled, renderLaunchdPlist, renderSystemdSystemUnit, renderSystemdUserUnit, resolveHqBinary, resolveNodeBinary, parseSchtasksLastResult, parseWindowsTaskHqRoot, readRecordedWindowsHqRoot, schtasksLastResultFailed, uninstallDaemonService, windowsLaunchMode, windowsLogonNotice, } from "./install.js";
|
|
9
|
+
export { buildInstallPaths, daemonServiceStatus, detectPlatform, installDaemonIfStale, installDaemonService, isNewerCliVersion, readAssetTemplate, restartDaemonServiceIfInstalled, renderLaunchdPlist, renderSystemdSystemUnit, renderSystemdUserUnit, resolveAbsoluteNodeBinary, resolveHqBinary, resolveNodeBinary, resolveRunningCliEntry, parseSchtasksLastResult, parseWindowsTaskHqRoot, readRecordedWindowsHqRoot, schtasksLastResultFailed, uninstallDaemonService, windowsLaunchMode, windowsLogonNotice, } from "./install.js";
|
|
10
10
|
export { daemonConfigPath, findAncestorHqRoot, isInstallableHqRoot, readDaemonConfig, registerTrustedHqRoot, resolveInstallHqRoot, verifyHqRoot, } from "./config.js";
|
|
11
|
-
export { UNHEALTHY_SPOOL_AGE_MS, collectDaemonDoctor, formatDaemonDoctor, } from "./doctor.js";
|
|
11
|
+
export { UNHEALTHY_SPOOL_AGE_MS, collectDaemonDoctor, formatCliIdentityLines, formatCliMismatchLine, formatDaemonDoctor, listHqBinariesOnPath, } from "./doctor.js";
|
|
12
12
|
export { scopeBundleToAgentIdentity, } from "./credentials.js";
|
|
13
13
|
export { FLUSH_INTERVAL_MS, DAEMON_VERSION_DRIFT_EXIT_CODE, SPOOL_DEBOUNCE_MS, runMeshDaemon, } from "./run.js";
|
|
14
14
|
export { TRANSCRIPT_ADAPTER_VERSION, TRANSCRIPT_EVENT_SOURCE, TRANSCRIPT_FRESH_MS, TRANSCRIPT_HOOK_COVER_MS, TRANSCRIPT_MARKER, TRANSCRIPT_SESSION_END_MS, TRANSCRIPT_TURN_QUIET_MS, TRANSCRIPT_WATCH_BACKOFF_DURATION_MS, TRANSCRIPT_WATCH_BACKOFF_ENTRIES, TRANSCRIPT_WATCH_ENTRY_CAP, TRANSCRIPT_WATCH_INTERVAL_MAX_MS, TRANSCRIPT_WATCH_INTERVAL_MS, TRANSCRIPT_WATCH_REVALIDATE_MS, TranscriptWatcher, createTranscriptDiscoverCache, decodeClaudeProjectDirName, defaultTranscriptFs, discoverTranscripts, discoverTranscriptsWithMeta, isTranscriptHookCovered, nextTranscriptWatchIntervalMs, noteHookSessionsFromSpoolFile, resolveTranscriptRegistration, sessionIdFromClaudeTranscriptPath, sessionIdFromCodexTranscriptPath, sortDiscoveries, } from "./transcript-watch.js";
|