@indigoai-us/hq-cli 5.292.0 → 5.293.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 +26 -0
- package/dist/command-catalog.generated.d.ts +3 -0
- package/dist/command-catalog.generated.js +4 -0
- package/dist/commands/lanes-move.js +54 -1
- package/dist/commands/lanes-questions.js +4 -0
- package/dist/commands/lanes.d.ts +2 -0
- package/dist/commands/lanes.js +119 -5
- package/dist/lib/core-utils/sentry-report.js +2 -2
- package/dist/lib/daemon/run.d.ts +11 -0
- package/dist/lib/daemon/run.js +17 -1
- package/dist/lib/lanes/autopilot.d.ts +13 -1
- package/dist/lib/lanes/autopilot.js +271 -28
- package/dist/lib/lanes/grants.d.ts +1 -1
- package/dist/lib/lanes/grants.js +7 -5
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.d.ts +5 -0
- package/dist/lib/lanes/keep-alive.js +9 -4
- package/dist/lib/lanes/questions.d.ts +2 -1
- package/dist/lib/lanes/questions.js +2 -0
- package/dist/lib/lanes/reconcile.js +186 -9
- package/dist/lib/lanes/spawn.js +4 -0
- package/dist/lib/lanes/store.js +21 -1
- package/dist/lib/lanes/types.d.ts +13 -1
- package/dist/lib/lanes/types.js +2 -2
- package/dist/lib/lanes/watcher.js +3 -1
- package/dist/utils/self-update.d.ts +7 -0
- package/dist/utils/self-update.js +5 -1
- package/dist/utils/version-gate.d.ts +15 -0
- package/dist/utils/version-gate.js +46 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.293.0] — 2026-09-30
|
|
6
|
+
- [HQ-CLI-HOOK-TIMEOUT-KIND] Group hook timeout events by normalized kind (`warning` or `exceeded`) across scripts; keep script and platform tags for triage. Existing per-script issues will stop receiving new events, and the lead handles their status.
|
|
7
|
+
- `hq lanes move` now reports an already-on-target no-op without dispatching or changing the queued lane.
|
|
8
|
+
- On Linux, `hq daemon run` sets XDG_RUNTIME_DIR for its services when the system unit left it unset, so `systemctl --user` from jobs-reconcile no longer fails with "Failed to connect to bus: No medium found".
|
|
9
|
+
|
|
10
|
+
- With the default-off lanes.keep-alive-resume-blocked gate enabled, reconcile
|
|
11
|
+
resumes an opted-in blocked lead after 15 minutes when it has open stories and
|
|
12
|
+
no pending question. After two blocked resumes, it records another blocked
|
|
13
|
+
episode for operator review.
|
|
14
|
+
- Lead autopilot dry runs no longer start workers, append repeat wakes, or write escalation records. Passed stories count as closed and canonical story-ledger lane objects are accepted.
|
|
15
|
+
- Autopilot checks status paths against the lead company, scopes escalation deduplication to each lead, includes carried stories in stale-worker checks, and recovers locks left by reused PIDs.
|
|
16
|
+
- The lead autopilot adds prompts for available grant slots, flat KPI reports, stale blockers, completed projects, stale worker ledgers, and stories without live lanes, with escalation after two unanswered wakes.
|
|
17
|
+
|
|
18
|
+
- Preserve empty directory paths and modes in recorded corpus cases. The recorder rejects tree entries the case format cannot replay, and CI checks committed snapshots after a clean Git archive extraction.
|
|
19
|
+
- Cross-Outpost resume and hire-grant dispatch verify the senior from the
|
|
20
|
+
senior's owning Outpost before signing the caller's grant context.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- Add `hq lanes ask --priority P0|P1|P2` and show question urgency in the queue; warn when omitted during the compatibility release.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- CI-only: keep the same-binary Node control case measured without applying performance regression thresholds.
|
|
29
|
+
- Fix npm global self-updates, which have failed with MODULE_NOT_FOUND since 5.290.0. The update moved the hq-cli package aside and then started its installer through a supervisor script inside that package. It now runs the installer through a copy of the supervisor outside the package.
|
|
30
|
+
|
|
5
31
|
## [5.292.0] — 2026-09-30
|
|
6
32
|
- Add native `hq core jobs reconcile` behind `core-native-jobs`, retaining the bundled shell rollback and scheduled-job helpers.
|
|
7
33
|
- Behind the default-off `cli.staged-self-update` flag, an npm global self-update installs the new version beside the running package, checks that it answers `--version`, then swaps it in with two renames. The running `hq` stays on disk for the whole install, so services the daemon starts during an update no longer fail with "hq: command not found" or MODULE_NOT_FOUND. pnpm, Bun, Windows and linked installs keep the in-place update.
|
|
@@ -8655,6 +8655,9 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
8655
8655
|
}, {
|
|
8656
8656
|
readonly flags: "--recommended <text>";
|
|
8657
8657
|
readonly description: "Recommended option";
|
|
8658
|
+
}, {
|
|
8659
|
+
readonly flags: "--priority <P0|P1|P2>";
|
|
8660
|
+
readonly description: "Urgency: P0 blocks now, P1 needed today, P2 can wait";
|
|
8658
8661
|
}, {
|
|
8659
8662
|
readonly flags: "--json";
|
|
8660
8663
|
readonly description: "JSON on stdout (always)";
|
|
@@ -11171,6 +11171,10 @@ export const COMMAND_CATALOG = [
|
|
|
11171
11171
|
"flags": "--recommended <text>",
|
|
11172
11172
|
"description": "Recommended option"
|
|
11173
11173
|
},
|
|
11174
|
+
{
|
|
11175
|
+
"flags": "--priority <P0|P1|P2>",
|
|
11176
|
+
"description": "Urgency: P0 blocks now, P1 needed today, P2 can wait"
|
|
11177
|
+
},
|
|
11174
11178
|
{
|
|
11175
11179
|
"flags": "--json",
|
|
11176
11180
|
"description": "JSON on stdout (always)"
|
|
@@ -19,6 +19,38 @@ export function createLaneMoveHandlers(deps) {
|
|
|
19
19
|
lane.last_host_report_at === undefined &&
|
|
20
20
|
lane.pending_resume === undefined;
|
|
21
21
|
}
|
|
22
|
+
function sameHost(a, b) {
|
|
23
|
+
return a.kind === b.kind && a.id === b.id;
|
|
24
|
+
}
|
|
25
|
+
function reportAlreadyOnTarget(lane) {
|
|
26
|
+
printJson({
|
|
27
|
+
ok: true,
|
|
28
|
+
action: "move",
|
|
29
|
+
lane_id: lane.lane_id,
|
|
30
|
+
state: lane.state,
|
|
31
|
+
host: lane.host,
|
|
32
|
+
account: lane.account ?? null,
|
|
33
|
+
moved: false,
|
|
34
|
+
reason: "already_on_target",
|
|
35
|
+
lane,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async function reportAlreadyOnTargetIfCurrent(hqRoot, laneId, provider, target, callerHost, runtimeHost, ctx) {
|
|
39
|
+
const current = await withLaneAdmissionLock(hqRoot, provider, async () => {
|
|
40
|
+
const lane = readLane(hqRoot, laneId);
|
|
41
|
+
if (!laneCanMoveBeforeStart(lane)) {
|
|
42
|
+
throw new LanesError(lane.state === "queued" ? "lane_move_started" : "lane_not_queued", `Lane ${laneId} cannot move after it has started; current state is ${lane.state}`, { lane_id: laneId, state: lane.state });
|
|
43
|
+
}
|
|
44
|
+
return sameHost(lane.host, target) ||
|
|
45
|
+
(laneRunsHere(lane, runtimeHost) && sameHost(callerHost, target))
|
|
46
|
+
? lane
|
|
47
|
+
: undefined;
|
|
48
|
+
}, ctx.sleep);
|
|
49
|
+
if (current === undefined)
|
|
50
|
+
return false;
|
|
51
|
+
reportAlreadyOnTarget(current);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
22
54
|
function readLaneTimeout(hqRoot, laneId) {
|
|
23
55
|
const raw = readJsonFile(timeoutPath(hqRoot, laneId));
|
|
24
56
|
if (raw === null)
|
|
@@ -51,6 +83,22 @@ export function createLaneMoveHandlers(deps) {
|
|
|
51
83
|
throw new LanesError(source.state === "queued" ? "lane_move_started" : "lane_not_queued", `Lane ${laneId} cannot move after it has started; current state is ${source.state}`, { lane_id: laneId, state: source.state });
|
|
52
84
|
}
|
|
53
85
|
const sourceHost = source.host;
|
|
86
|
+
const runtimeHost = reconcileHostFromCtx(ctx);
|
|
87
|
+
const runningHere = laneRunsHere(source, runtimeHost);
|
|
88
|
+
const callerHost = runtimeHost.outpostId
|
|
89
|
+
? { kind: "outpost", id: runtimeHost.outpostId }
|
|
90
|
+
: { kind: "local", id: runtimeHost.hostname };
|
|
91
|
+
const explicitTarget = policy.kind === "local"
|
|
92
|
+
? callerHost
|
|
93
|
+
: policy.kind === "outpost"
|
|
94
|
+
? { kind: "outpost", id: policy.id }
|
|
95
|
+
: undefined;
|
|
96
|
+
if (explicitTarget !== undefined &&
|
|
97
|
+
(sameHost(sourceHost, explicitTarget) ||
|
|
98
|
+
(runningHere && sameHost(callerHost, explicitTarget)))) {
|
|
99
|
+
if (await reportAlreadyOnTargetIfCurrent(hqRoot, laneId, source.provider, explicitTarget, callerHost, runtimeHost, ctx))
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
54
102
|
const config = loadLanesConfig(hqRoot);
|
|
55
103
|
const capacity = await loadHostCapacitySnapshots(ctx, hqRoot, config);
|
|
56
104
|
let targetSnapshots = capacity.snapshots.filter((snapshot) => snapshot.host.kind !== sourceHost.kind || snapshot.host.id !== sourceHost.id);
|
|
@@ -69,8 +117,13 @@ export function createLaneMoveHandlers(deps) {
|
|
|
69
117
|
...(source.account !== undefined ? { account: source.account } : {}),
|
|
70
118
|
...(allowedAccounts !== undefined ? { allowedAccounts } : {}),
|
|
71
119
|
});
|
|
120
|
+
if (sameHost(sourceHost, choice.host) ||
|
|
121
|
+
(runningHere && sameHost(callerHost, choice.host))) {
|
|
122
|
+
if (await reportAlreadyOnTargetIfCurrent(hqRoot, laneId, source.provider, choice.host, callerHost, runtimeHost, ctx))
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
72
125
|
let moved;
|
|
73
|
-
if (
|
|
126
|
+
if (runningHere) {
|
|
74
127
|
moved = await commitLaneMove(ctx, laneId, choice.host, choice.account, placementEnabled);
|
|
75
128
|
}
|
|
76
129
|
else if (source.host.kind === "outpost") {
|
|
@@ -475,6 +475,8 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
475
475
|
.map((q) => {
|
|
476
476
|
const row = {
|
|
477
477
|
...q,
|
|
478
|
+
priority: q.priority ?? null,
|
|
479
|
+
priority_display: q.priority ?? "-",
|
|
478
480
|
audience: resolveAudience(hqRoot, q, ctx),
|
|
479
481
|
...(q.question_kind === "continue" ? { flag: "continue-loop" } : {}),
|
|
480
482
|
};
|
|
@@ -503,6 +505,8 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
503
505
|
})
|
|
504
506
|
.map(({ question: q, host, audience }) => ({
|
|
505
507
|
...q,
|
|
508
|
+
priority: q.priority ?? null,
|
|
509
|
+
priority_display: q.priority ?? "-",
|
|
506
510
|
host,
|
|
507
511
|
audience,
|
|
508
512
|
...(q.question_kind === "continue" ? { flag: "continue-loop" } : {}),
|
package/dist/commands/lanes.d.ts
CHANGED
|
@@ -114,6 +114,8 @@ export declare function parseQuestionBody(raw: unknown): QuestionBody | null;
|
|
|
114
114
|
*/
|
|
115
115
|
export declare const ASK_BACKOFF_MS: readonly [5000, 15000];
|
|
116
116
|
export declare const ASK_ATTEMPTS: number;
|
|
117
|
+
/** Flip to true in the next release to make --priority mandatory. */
|
|
118
|
+
export declare const ASK_PRIORITY_REQUIRED = false;
|
|
117
119
|
export declare function casUpdateQuestion(hqRoot: string, questionId: string, updater: (current: QuestionRecord) => QuestionRecord): QuestionRecord;
|
|
118
120
|
export declare function casUpdateLane(hqRoot: string, laneId: string, updater: (current: LaneRecord) => LaneRecord): LaneRecord;
|
|
119
121
|
/**
|
package/dist/commands/lanes.js
CHANGED
|
@@ -490,6 +490,67 @@ function mergeOwnerLaneHierarchy(supplied, ownerLanes) {
|
|
|
490
490
|
byId.set(lane.lane_id, lane);
|
|
491
491
|
return [...byId.values()];
|
|
492
492
|
}
|
|
493
|
+
function hierarchyWithRemoteSenior(hierarchy, remoteSenior) {
|
|
494
|
+
return [
|
|
495
|
+
...hierarchy.filter((lane) => lane.lane_id !== remoteSenior.lane_id),
|
|
496
|
+
remoteSenior,
|
|
497
|
+
];
|
|
498
|
+
}
|
|
499
|
+
async function readRemoteHireGrantSenior(ctx, hqRoot, seniorLaneId) {
|
|
500
|
+
if (!listLaneIds(hqRoot).includes(seniorLaneId))
|
|
501
|
+
return undefined;
|
|
502
|
+
const mirror = readLane(hqRoot, seniorLaneId);
|
|
503
|
+
if (mirror.host.kind !== "outpost" ||
|
|
504
|
+
laneRunsHere(mirror, reconcileHostFromCtx(ctx))) {
|
|
505
|
+
return undefined;
|
|
506
|
+
}
|
|
507
|
+
const outpostId = mirror.host.id;
|
|
508
|
+
const unavailable = (reason) => new LanesError("hire_grant_senior_unavailable", `Cannot verify senior lane ${seniorLaneId} from owning Outpost ${outpostId}; refusing hire-grant admission (${reason})`, { senior_lane_id: seniorLaneId, outpost_id: outpostId, reason });
|
|
509
|
+
let ownerRecords;
|
|
510
|
+
try {
|
|
511
|
+
ownerRecords = await readOutpostLaneRecords(ctx, outpostId, {
|
|
512
|
+
laneIds: [seniorLaneId],
|
|
513
|
+
snapshot: true,
|
|
514
|
+
strictSnapshot: true,
|
|
515
|
+
}, {}, { bypassCache: true });
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
throw unavailable(isLanesError(error) ? error.error : "outpost_lane_list_unavailable");
|
|
519
|
+
}
|
|
520
|
+
const matches = ownerRecords.filter((lane) => lane.lane_id === seniorLaneId);
|
|
521
|
+
const owner = matches[0];
|
|
522
|
+
const ownerIsHostedByOutpost = owner !== undefined && ((owner.host.kind === "outpost" && owner.host.id === outpostId) ||
|
|
523
|
+
(owner.host.kind === "local" && owner.host.id.trim() !== ""));
|
|
524
|
+
if (matches.length !== 1 || owner === undefined || !ownerIsHostedByOutpost) {
|
|
525
|
+
throw unavailable(matches.length === 0
|
|
526
|
+
? "owner_did_not_return_senior"
|
|
527
|
+
: matches.length > 1
|
|
528
|
+
? "owner_returned_duplicate_seniors"
|
|
529
|
+
: "owner_senior_identity_mismatch");
|
|
530
|
+
}
|
|
531
|
+
try {
|
|
532
|
+
const mirrorCompany = resolveLaneCompany(hqRoot, mirror).company;
|
|
533
|
+
const ownerCompany = resolveLaneCompany(hqRoot, owner).company;
|
|
534
|
+
if ((ownerCompany?.uid ?? null) !== (mirrorCompany?.uid ?? null) ||
|
|
535
|
+
(ownerCompany?.slug ?? null) !== (mirrorCompany?.slug ?? null)) {
|
|
536
|
+
throw unavailable("owner_company_did_not_match_mirror");
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
if (isLanesError(error) && error.error === "hire_grant_senior_unavailable")
|
|
541
|
+
throw error;
|
|
542
|
+
throw unavailable("owner_company_identity_unavailable");
|
|
543
|
+
}
|
|
544
|
+
return owner;
|
|
545
|
+
}
|
|
546
|
+
async function hierarchyWithVerifiedRemoteSenior(ctx, hqRoot, lane, hierarchy) {
|
|
547
|
+
if (lane.senior.kind !== "lane" || !lane.senior.id)
|
|
548
|
+
return [...hierarchy];
|
|
549
|
+
const remoteSenior = await readRemoteHireGrantSenior(ctx, hqRoot, lane.senior.id);
|
|
550
|
+
return remoteSenior === undefined
|
|
551
|
+
? [...hierarchy]
|
|
552
|
+
: mergeOwnerLaneRecords(hierarchy, [remoteSenior]);
|
|
553
|
+
}
|
|
493
554
|
function mergeOwnerLaneRecords(supplied, ownerLanes) {
|
|
494
555
|
const byId = new Map(supplied.map((lane) => [lane.lane_id, lane]));
|
|
495
556
|
for (const lane of ownerLanes)
|
|
@@ -911,8 +972,18 @@ async function withResumeHireAdmission(ctx, lane, fn, grantContext) {
|
|
|
911
972
|
throw new LanesError("hire_grant_context_required", `Resume of Outpost lane ${lane.lane_id} requires the primary senior's hire-grant context`, { lane_id: lane.lane_id, senior_lane_id: lane.senior.id });
|
|
912
973
|
}
|
|
913
974
|
await refreshRemoteLaneMirrors(ctx, hqRoot, loadAllLanes(hqRoot), new Set([lane.senior.id]));
|
|
975
|
+
const remoteSenior = grantContext === undefined
|
|
976
|
+
? await readRemoteHireGrantSenior(ctx, hqRoot, lane.senior.id)
|
|
977
|
+
: undefined;
|
|
978
|
+
const suppliedHierarchy = grantContext?.hierarchy ?? (remoteSenior === undefined
|
|
979
|
+
? undefined
|
|
980
|
+
: hierarchyWithRemoteSenior(loadAllLanes(hqRoot), remoteSenior));
|
|
914
981
|
return withHireGrantAdmission(hqRoot, lane.senior.id, (snapshot) => {
|
|
915
982
|
const current = readLane(hqRoot, lane.lane_id);
|
|
983
|
+
if (remoteSenior !== undefined &&
|
|
984
|
+
(current.senior.kind !== "lane" || current.senior.id !== remoteSenior.lane_id)) {
|
|
985
|
+
throw new LanesError("hire_grant_senior_changed", `Lane ${current.lane_id}'s senior changed while its owning Outpost was being checked`, { lane_id: current.lane_id, expected_senior_lane_id: remoteSenior.lane_id, current_senior: current.senior });
|
|
986
|
+
}
|
|
916
987
|
if (grantContext !== undefined &&
|
|
917
988
|
!sameHireGrantSenior(current.senior, grantContext.expected_senior)) {
|
|
918
989
|
throw new LanesError("hire_grant_senior_changed", `Lane ${current.lane_id}'s senior changed before the Outpost resumed it`, {
|
|
@@ -929,7 +1000,7 @@ async function withResumeHireAdmission(ctx, lane, fn, grantContext) {
|
|
|
929
1000
|
if (refusal !== null)
|
|
930
1001
|
throw refusal;
|
|
931
1002
|
return fn();
|
|
932
|
-
},
|
|
1003
|
+
}, suppliedHierarchy, remoteSenior === undefined ? undefined : new Set([remoteSenior.lane_id]));
|
|
933
1004
|
}
|
|
934
1005
|
const CAPACITY_PROBE_TIMEOUT_MS = 15_000;
|
|
935
1006
|
const MAX_PLACEMENT_OUTPOSTS = 16;
|
|
@@ -1205,8 +1276,11 @@ async function readOutpostLaneRecordsUncached(ctx, outpostId, query, dispatchOpt
|
|
|
1205
1276
|
}
|
|
1206
1277
|
return lanes;
|
|
1207
1278
|
}
|
|
1208
|
-
async function readOutpostLaneRecords(ctx, outpostId, requestedQuery = {}, dispatchOptions = {}) {
|
|
1279
|
+
async function readOutpostLaneRecords(ctx, outpostId, requestedQuery = {}, dispatchOptions = {}, options = {}) {
|
|
1209
1280
|
const query = normalizedLaneListQuery(requestedQuery);
|
|
1281
|
+
if (options.bypassCache === true) {
|
|
1282
|
+
return readOutpostLaneRecordsUncached(ctx, outpostId, query, dispatchOptions);
|
|
1283
|
+
}
|
|
1210
1284
|
let invocationMemo = outpostLaneListMemo.get(ctx);
|
|
1211
1285
|
if (!invocationMemo) {
|
|
1212
1286
|
invocationMemo = new OutpostSnapshotInvocationMemo();
|
|
@@ -2144,6 +2218,10 @@ function assertKnownOutpost(requested, known) {
|
|
|
2144
2218
|
*/
|
|
2145
2219
|
export const ASK_BACKOFF_MS = [5_000, 15_000];
|
|
2146
2220
|
export const ASK_ATTEMPTS = ASK_BACKOFF_MS.length + 1;
|
|
2221
|
+
/** Flip to true in the next release to make --priority mandatory. */
|
|
2222
|
+
export const ASK_PRIORITY_REQUIRED = false;
|
|
2223
|
+
const ASK_PRIORITY_WARNING = "--priority will be required in the next release.";
|
|
2224
|
+
const QUESTION_PRIORITIES = ["P0", "P1", "P2"];
|
|
2147
2225
|
function withCasRetry(fn, attempts = 5) {
|
|
2148
2226
|
let last;
|
|
2149
2227
|
for (let i = 0; i < attempts; i += 1) {
|
|
@@ -3980,19 +4058,21 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
|
|
|
3980
4058
|
let grantContext;
|
|
3981
4059
|
let grantHierarchy;
|
|
3982
4060
|
if (action === "grant") {
|
|
3983
|
-
const
|
|
4061
|
+
const ownerHierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneRecords(ctx, outpostId, {
|
|
3984
4062
|
hierarchy: [lane.lane_id],
|
|
3985
4063
|
snapshot: true,
|
|
3986
4064
|
}));
|
|
4065
|
+
const hierarchy = await hierarchyWithVerifiedRemoteSenior(ctx, ctx.hqRoot(), lane, ownerHierarchy);
|
|
3987
4066
|
grantHierarchy = hierarchy;
|
|
3988
4067
|
grantContext = createOutpostHireGrantContext(lane, hierarchy);
|
|
3989
4068
|
}
|
|
3990
4069
|
else if ((action === "resume" && lane.senior.kind === "lane") ||
|
|
3991
4070
|
(action === "message" && (lane.senior.kind === "lane" || lane.senior.kind === "session"))) {
|
|
3992
|
-
const
|
|
4071
|
+
const ownerHierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneRecords(ctx, outpostId, {
|
|
3993
4072
|
hierarchy: [lane.lane_id],
|
|
3994
4073
|
snapshot: true,
|
|
3995
4074
|
}));
|
|
4075
|
+
const hierarchy = await hierarchyWithVerifiedRemoteSenior(ctx, ctx.hqRoot(), lane, ownerHierarchy);
|
|
3996
4076
|
grantContext = createOutpostHireGrantContext(lane, hierarchy, action === "message" ? options.supersededMessageCallerId : undefined, action === "message" ? options.supersededMessageSessionId : undefined);
|
|
3997
4077
|
}
|
|
3998
4078
|
let dispatched;
|
|
@@ -5254,6 +5334,10 @@ async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContex
|
|
|
5254
5334
|
}
|
|
5255
5335
|
await refreshRemoteLaneMirrors(ctx, hqRoot, loadAllLanes(hqRoot), parentLaneIds);
|
|
5256
5336
|
}
|
|
5337
|
+
const remoteSenior = authorization === "senior" &&
|
|
5338
|
+
initial.senior.kind === "lane" && initial.senior.id
|
|
5339
|
+
? await readRemoteHireGrantSenior(ctx, hqRoot, initial.senior.id)
|
|
5340
|
+
: undefined;
|
|
5257
5341
|
const updated = ctx.withGrantHireLocks(hqRoot, lockedLaneIds, () => {
|
|
5258
5342
|
const current = readLane(hqRoot, laneId);
|
|
5259
5343
|
const currentCallerAuthorization = authorization === "senior"
|
|
@@ -5270,11 +5354,18 @@ async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContex
|
|
|
5270
5354
|
(!grantContext || !sameHireGrantSenior(current.senior, grantContext.expected_senior))) {
|
|
5271
5355
|
throw new LanesError("hire_grant_senior_changed", `Lane ${laneId}'s senior changed before the Outpost applied the grant update`, { lane_id: laneId, current_senior: current.senior });
|
|
5272
5356
|
}
|
|
5273
|
-
|
|
5357
|
+
if (remoteSenior !== undefined &&
|
|
5358
|
+
(current.senior.kind !== "lane" || current.senior.id !== remoteSenior.lane_id)) {
|
|
5359
|
+
throw new LanesError("hire_grant_senior_changed", `Lane ${current.lane_id}'s senior changed while its owning Outpost was being checked`, { lane_id: current.lane_id, expected_senior_lane_id: remoteSenior.lane_id, current_senior: current.senior });
|
|
5360
|
+
}
|
|
5361
|
+
const currentHierarchy = grantContext
|
|
5274
5362
|
? authorization === "ssm-owner-dispatch"
|
|
5275
5363
|
? mergeOwnerLaneHierarchy(grantContext.hierarchy, loadAllLanes(hqRoot))
|
|
5276
5364
|
: grantContext.hierarchy
|
|
5277
5365
|
: loadAllLanes(hqRoot);
|
|
5366
|
+
const hierarchy = remoteSenior === undefined
|
|
5367
|
+
? currentHierarchy
|
|
5368
|
+
: hierarchyWithRemoteSenior(currentHierarchy, remoteSenior);
|
|
5278
5369
|
if (current.senior.kind === "lane") {
|
|
5279
5370
|
if (!current.senior.id) {
|
|
5280
5371
|
throw new LanesError("hire_grant_senior_invalid", `Lane ${laneId} has a lane senior without an id`, {
|
|
@@ -7443,7 +7534,23 @@ export function interruptNotResumableWarning(laneId) {
|
|
|
7443
7534
|
return `hq lanes: warning: lane ${laneId} was interrupted before its provider reported a session id, ` +
|
|
7444
7535
|
`so hq lanes resume cannot continue it. Start a new lane for the remaining work.\n`;
|
|
7445
7536
|
}
|
|
7537
|
+
function parseQuestionPriority(value) {
|
|
7538
|
+
if (value === undefined) {
|
|
7539
|
+
if (ASK_PRIORITY_REQUIRED) {
|
|
7540
|
+
throw new LanesError("ask_priority_required", "--priority is required; choose P0, P1, or P2", {
|
|
7541
|
+
allowed: [...QUESTION_PRIORITIES],
|
|
7542
|
+
});
|
|
7543
|
+
}
|
|
7544
|
+
return undefined;
|
|
7545
|
+
}
|
|
7546
|
+
const normalized = value.toUpperCase();
|
|
7547
|
+
if (!QUESTION_PRIORITIES.includes(normalized)) {
|
|
7548
|
+
throw new LanesError("ask_priority_invalid", `Invalid --priority ${JSON.stringify(value)}. Allowed values: P0, P1, P2`, { allowed: [...QUESTION_PRIORITIES], value });
|
|
7549
|
+
}
|
|
7550
|
+
return normalized;
|
|
7551
|
+
}
|
|
7446
7552
|
async function runAsk(ctx, opts) {
|
|
7553
|
+
const priority = parseQuestionPriority(opts.priority);
|
|
7447
7554
|
const hqRoot = ctx.hqRoot();
|
|
7448
7555
|
const laneId = ctx.env.HQ_LANE_ID?.trim();
|
|
7449
7556
|
if (!laneId) {
|
|
@@ -7482,6 +7589,7 @@ async function runAsk(ctx, opts) {
|
|
|
7482
7589
|
at,
|
|
7483
7590
|
activeCompany: questionCompany,
|
|
7484
7591
|
questionId,
|
|
7592
|
+
priority,
|
|
7485
7593
|
});
|
|
7486
7594
|
questionId = withBody.question_id;
|
|
7487
7595
|
await emitLaneMeshEvent(ctx, lane.mesh_company ?? lane.company, lane.thread_id, "lane_question", {
|
|
@@ -7496,12 +7604,16 @@ async function runAsk(ctx, opts) {
|
|
|
7496
7604
|
question_id: withBody.question_id,
|
|
7497
7605
|
});
|
|
7498
7606
|
}
|
|
7607
|
+
if (priority === undefined) {
|
|
7608
|
+
process.stderr.write(`hq lanes ask: warning: ${ASK_PRIORITY_WARNING}\n`);
|
|
7609
|
+
}
|
|
7499
7610
|
printJson({
|
|
7500
7611
|
ok: true,
|
|
7501
7612
|
action: "ask",
|
|
7502
7613
|
question: withBody,
|
|
7503
7614
|
lane_id: lane.lane_id,
|
|
7504
7615
|
lane_state: lane.state,
|
|
7616
|
+
...(priority === undefined ? { warnings: [ASK_PRIORITY_WARNING] } : {}),
|
|
7505
7617
|
});
|
|
7506
7618
|
return;
|
|
7507
7619
|
}
|
|
@@ -8493,6 +8605,8 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8493
8605
|
.option("--context <text>", "Context for the audience")
|
|
8494
8606
|
.option("--option <text>", "Answer choice (repeatable)", collectRepeatable)
|
|
8495
8607
|
.option("--recommended <text>", "Recommended option")
|
|
8608
|
+
.option("--priority <P0|P1|P2>", "Urgency: P0 blocks now, P1 needed today, P2 can wait")
|
|
8609
|
+
.addHelpText("after", "\nThe audience may take up to 24 hours to answer; keep working meanwhile.\n")
|
|
8496
8610
|
.action(wrap(async (opts) => {
|
|
8497
8611
|
await runAsk(ctx, opts);
|
|
8498
8612
|
}));
|
|
@@ -677,13 +677,13 @@ function scrubbedSentryEnvelope(event, overrides, additionalTags = {}) {
|
|
|
677
677
|
...sentryEnvelope(event, overrides, additionalTags),
|
|
678
678
|
}, {});
|
|
679
679
|
}
|
|
680
|
-
/**
|
|
680
|
+
/** Group hook-timeout conditions by normalized kind while retaining triage tags. */
|
|
681
681
|
function hookTimeoutExceededOverrides(event) {
|
|
682
682
|
if (!isHookTimeoutEvent(event))
|
|
683
683
|
return undefined;
|
|
684
684
|
return {
|
|
685
685
|
level: "warning",
|
|
686
|
-
fingerprint: ["hook-timeout",
|
|
686
|
+
fingerprint: ["hook-timeout", hookTimeoutKind(event)],
|
|
687
687
|
};
|
|
688
688
|
}
|
|
689
689
|
/**
|
package/dist/lib/daemon/run.d.ts
CHANGED
|
@@ -62,5 +62,16 @@ export declare function rotateLogs(logsDir: string, maxBytes?: number): number;
|
|
|
62
62
|
* an on-failure unit down. The daemon's updater handles CLI updates.
|
|
63
63
|
*/
|
|
64
64
|
export declare function unitProcessEnv(base: NodeJS.ProcessEnv, unit: UnitPlan): NodeJS.ProcessEnv;
|
|
65
|
+
/**
|
|
66
|
+
* A systemd system unit starts the daemon as the login user without the
|
|
67
|
+
* variables a login session sets, so `systemctl --user` from its services
|
|
68
|
+
* cannot find the user bus ("Failed to connect to bus: No medium found").
|
|
69
|
+
* Point them at the user's runtime dir when it exists and nothing set one.
|
|
70
|
+
*/
|
|
71
|
+
export declare function withUserRuntimeDir(env: NodeJS.ProcessEnv, host: {
|
|
72
|
+
platform: NodeJS.Platform;
|
|
73
|
+
uid: number;
|
|
74
|
+
exists: (dir: string) => boolean;
|
|
75
|
+
}): NodeJS.ProcessEnv;
|
|
65
76
|
export declare function runDaemon(opts: RunDaemonOptions): Promise<number>;
|
|
66
77
|
//# sourceMappingURL=run.d.ts.map
|
package/dist/lib/daemon/run.js
CHANGED
|
@@ -233,8 +233,24 @@ function isFifo(file) {
|
|
|
233
233
|
return false;
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* A systemd system unit starts the daemon as the login user without the
|
|
238
|
+
* variables a login session sets, so `systemctl --user` from its services
|
|
239
|
+
* cannot find the user bus ("Failed to connect to bus: No medium found").
|
|
240
|
+
* Point them at the user's runtime dir when it exists and nothing set one.
|
|
241
|
+
*/
|
|
242
|
+
export function withUserRuntimeDir(env, host) {
|
|
243
|
+
if (host.platform !== "linux" || host.uid === 0 || env.XDG_RUNTIME_DIR)
|
|
244
|
+
return env;
|
|
245
|
+
const dir = `/run/user/${host.uid}`;
|
|
246
|
+
return host.exists(dir) ? { ...env, XDG_RUNTIME_DIR: dir } : env;
|
|
247
|
+
}
|
|
236
248
|
export async function runDaemon(opts) {
|
|
237
|
-
const env = opts.env ?? process.env
|
|
249
|
+
const env = withUserRuntimeDir(opts.env ?? process.env, {
|
|
250
|
+
platform: process.platform,
|
|
251
|
+
uid: process.getuid?.() ?? -1,
|
|
252
|
+
exists: fs.existsSync,
|
|
253
|
+
});
|
|
238
254
|
const home = opts.home ?? os.homedir();
|
|
239
255
|
const platform = process.platform;
|
|
240
256
|
const paths = daemonPaths(home, env);
|
|
@@ -12,8 +12,9 @@ export interface RegisteredLead {
|
|
|
12
12
|
status: string;
|
|
13
13
|
prd: string;
|
|
14
14
|
}
|
|
15
|
+
export type AutopilotCondition = "review_finished_worker" | "fill_grant" | "plan_free_slots" | "flat_kpi" | "blocker_record_needs_update" | "project_ready_to_close" | "stale_worker_ledger" | "story_without_live_lane";
|
|
15
16
|
export interface AutopilotWake {
|
|
16
|
-
condition:
|
|
17
|
+
condition: AutopilotCondition;
|
|
17
18
|
lead_lane: string;
|
|
18
19
|
story_id: string;
|
|
19
20
|
worker_lane?: string;
|
|
@@ -30,6 +31,14 @@ export interface AutopilotPassResult {
|
|
|
30
31
|
story_id: string;
|
|
31
32
|
worker_lane: string;
|
|
32
33
|
}>;
|
|
34
|
+
escalated: Array<{
|
|
35
|
+
team: string;
|
|
36
|
+
lead_lane: string;
|
|
37
|
+
condition: AutopilotCondition;
|
|
38
|
+
story_id: string;
|
|
39
|
+
at: string;
|
|
40
|
+
fingerprint: string;
|
|
41
|
+
}>;
|
|
33
42
|
skipped: Array<{
|
|
34
43
|
lead_lane: string;
|
|
35
44
|
reason: string;
|
|
@@ -38,15 +47,18 @@ export interface AutopilotPassResult {
|
|
|
38
47
|
interface ProjectStory {
|
|
39
48
|
id: string;
|
|
40
49
|
status: string;
|
|
50
|
+
passes?: boolean;
|
|
41
51
|
briefPath?: string;
|
|
42
52
|
laneIds?: string[];
|
|
43
53
|
updatedAt?: string;
|
|
54
|
+
externalBlocker?: string;
|
|
44
55
|
}
|
|
45
56
|
export declare function planLeadAutopilotWakes(input: {
|
|
46
57
|
lead: LaneRecord;
|
|
47
58
|
children: readonly LaneRecord[];
|
|
48
59
|
stories: readonly ProjectStory[];
|
|
49
60
|
now: Date;
|
|
61
|
+
managerStatus?: Record<string, unknown>;
|
|
50
62
|
keepAliveResumed?: boolean;
|
|
51
63
|
}): AutopilotWake[];
|
|
52
64
|
export declare function runLeadAutopilotPass(input: {
|