@indigoai-us/hq-cli 5.311.1 → 5.311.2
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 +8 -0
- package/dist/commands/lanes-message.js +18 -7
- package/dist/commands/lanes-shared.d.ts +2 -0
- package/dist/commands/lanes-watch.js +1 -5
- package/dist/commands/lanes.js +4 -10
- package/dist/fast-scaffold.d.ts +4 -0
- package/dist/fast-scaffold.js +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/flag-registry.d.ts +2 -0
- package/dist/lib/flag-registry.js +19 -12
- package/dist/lib/lanes/autopilot.js +19 -2
- package/dist/lib/lanes/delivery.d.ts +1 -0
- package/dist/lib/lanes/keep-alive.d.ts +1 -3
- package/dist/lib/lanes/keep-alive.js +2 -4
- package/dist/lib/lanes/reconcile.js +14 -36
- package/dist/lib/lanes/spawn.d.ts +2 -0
- package/dist/lib/lanes/spawn.js +35 -12
- package/dist/lib/lanes/types.d.ts +2 -2
- package/dist/lib/lanes/watch.d.ts +0 -1
- package/dist/lib/lanes/watch.js +4 -9
- package/dist/main.js +1 -1
- package/dist/node-network-compat-core.d.ts +12 -0
- package/dist/node-network-compat-core.js +41 -0
- package/dist/node-network-compat-fast.d.ts +7 -0
- package/dist/node-network-compat-fast.js +23 -0
- package/dist/node-network-compat.d.ts +2 -18
- package/dist/node-network-compat.js +4 -39
- package/dist/node-preflight.d.ts +7 -1
- package/dist/node-preflight.js +5 -5
- package/dist/utils/hq-core-yaml.d.ts +10 -0
- package/dist/utils/hq-core-yaml.js +40 -0
- package/dist/utils/pack-contributions.d.ts +2 -9
- package/dist/utils/pack-contributions.js +1 -39
- package/dist/utils/scaffold-fast-import-guard.js +5 -1
- package/dist/utils/version-report.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.311.2] — 2026-10-01
|
|
6
|
+
- Reduce fast-path startup work by loading network compatibility only on routes that need it.
|
|
7
|
+
- Make envelope `next_check_at` an unconditional keep-alive hold and remove its completed rollout flag.
|
|
8
|
+
- Lead autopilot no longer asks a lead to review its own lane as a finished worker, or to relaunch a worker for a story assigned to the lead itself.
|
|
9
|
+
- Lead autopilot counts a story's current `lane` together with its `lanes` history, so `lanes: []` no longer hides the assigned lane.
|
|
10
|
+
- Keep the CLI company seed fixture byte-matched to the hq-core starter golden, including the session sentinel ignore rule.
|
|
11
|
+
- Record lane-watcher continuations as `watcher` resumes and label their prompt source accurately; record monitor-event and system-notice message sources distinctly in lane resume history.
|
|
12
|
+
|
|
5
13
|
## [5.311.1] — 2026-10-01
|
|
6
14
|
- Preserve shell-compatible diagnostics when delegate pickup receives a malformed manifest.
|
|
7
15
|
- Print URLs returned by `hq integrations call` without redacting their paths or query values. Other integration JSON output keeps its existing URL redaction.
|
|
@@ -448,6 +448,11 @@ export function createLaneMessageHandlers(deps) {
|
|
|
448
448
|
text: promptedText,
|
|
449
449
|
});
|
|
450
450
|
const deliveryText = framedMonitorEvent ?? promptedText;
|
|
451
|
+
const messageHistoryTrigger = opts.monitorEvent !== undefined
|
|
452
|
+
? "monitor_event"
|
|
453
|
+
: opts.messageSource ?? "message";
|
|
454
|
+
const isMonitorEvent = messageHistoryTrigger === "monitor_event";
|
|
455
|
+
const isSystemNotice = messageHistoryTrigger === "system_notice";
|
|
451
456
|
const dropboxMessage = {
|
|
452
457
|
at,
|
|
453
458
|
message_id: messageId,
|
|
@@ -562,9 +567,9 @@ export function createLaneMessageHandlers(deps) {
|
|
|
562
567
|
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
563
568
|
...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
|
|
564
569
|
pending: {
|
|
565
|
-
trigger:
|
|
570
|
+
trigger: messageHistoryTrigger,
|
|
566
571
|
text: deliveryText,
|
|
567
|
-
...(
|
|
572
|
+
...(isMonitorEvent ? { monitor_event: true } : {}),
|
|
568
573
|
message_id: messageId,
|
|
569
574
|
compact: opts.compact !== false,
|
|
570
575
|
},
|
|
@@ -589,9 +594,9 @@ export function createLaneMessageHandlers(deps) {
|
|
|
589
594
|
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
590
595
|
...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
|
|
591
596
|
pending: {
|
|
592
|
-
trigger:
|
|
597
|
+
trigger: messageHistoryTrigger,
|
|
593
598
|
text: deliveryText,
|
|
594
|
-
...(
|
|
599
|
+
...(isMonitorEvent ? { monitor_event: true } : {}),
|
|
595
600
|
message_id: messageId,
|
|
596
601
|
compact: opts.compact !== false,
|
|
597
602
|
},
|
|
@@ -674,10 +679,10 @@ export function createLaneMessageHandlers(deps) {
|
|
|
674
679
|
queued_at: at,
|
|
675
680
|
pending_resume: {
|
|
676
681
|
session_id: sessionId,
|
|
677
|
-
trigger:
|
|
682
|
+
trigger: messageHistoryTrigger,
|
|
678
683
|
caller: ctx.env.HQ_LANE_ID?.trim() || "user",
|
|
679
684
|
text: deliveryText,
|
|
680
|
-
...(
|
|
685
|
+
...(isMonitorEvent ? { monitor_event: true } : {}),
|
|
681
686
|
message_id: messageId,
|
|
682
687
|
compact: opts.compact !== false,
|
|
683
688
|
at,
|
|
@@ -708,7 +713,8 @@ export function createLaneMessageHandlers(deps) {
|
|
|
708
713
|
kind: "message",
|
|
709
714
|
text: deliveryText,
|
|
710
715
|
message_id: messageId,
|
|
711
|
-
...(
|
|
716
|
+
...(isMonitorEvent ? { monitor_event: true } : {}),
|
|
717
|
+
...(isSystemNotice ? { system_notice: true } : {}),
|
|
712
718
|
},
|
|
713
719
|
compact: opts.compact !== false,
|
|
714
720
|
effort: resumeEffort ?? recordedResumeEffort(lane),
|
|
@@ -868,6 +874,11 @@ export function createLaneMessageHandlers(deps) {
|
|
|
868
874
|
try {
|
|
869
875
|
await runMessage(ctx, laneId, {
|
|
870
876
|
text: formatDropboxMessages(messages),
|
|
877
|
+
messageSource: messages.some((message) => message.kind === "monitor_event")
|
|
878
|
+
? "monitor_event"
|
|
879
|
+
: messages.some((message) => message.kind === "system_notice")
|
|
880
|
+
? "system_notice"
|
|
881
|
+
: undefined,
|
|
871
882
|
compact: false,
|
|
872
883
|
silent: true,
|
|
873
884
|
onAccepted: () => { accepted = true; },
|
|
@@ -52,6 +52,8 @@ export interface LaneMessageOptions {
|
|
|
52
52
|
description: string;
|
|
53
53
|
sequence: number;
|
|
54
54
|
};
|
|
55
|
+
/** Source kind for a message recovered from the lane dropbox. */
|
|
56
|
+
messageSource?: "monitor_event" | "system_notice";
|
|
55
57
|
/** Internal recovery messages do not emit a second command response. */
|
|
56
58
|
silent?: boolean;
|
|
57
59
|
/** Internal recovery claim is safe to archive once this callback runs. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { accountStatus, coveredLaneIds, loadLanesConfig, MAX_WATCHED_LANES, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, monitorReminderText, runLaneWatch, } from "../lib/lanes/index.js";
|
|
2
|
-
import { LEAD_LANE_KEEP_ALIVE_FLAG
|
|
2
|
+
import { LEAD_LANE_KEEP_ALIVE_FLAG } from "../lib/lanes/keep-alive.js";
|
|
3
3
|
import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
|
|
4
4
|
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
5
5
|
import { isLanesError, LanesError } from "../lib/lanes/errors.js";
|
|
@@ -50,10 +50,6 @@ export function createLaneWatchHandlers(deps) {
|
|
|
50
50
|
company: resolveLaneCompany(hqRoot, lane).company ?? undefined,
|
|
51
51
|
env: ctx.env,
|
|
52
52
|
}),
|
|
53
|
-
keepAliveNextCheckAtEnabled: (lane) => readLanesRegistryFlag(LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_FLAG, {
|
|
54
|
-
company: resolveLaneCompany(hqRoot, lane).company ?? undefined,
|
|
55
|
-
env: ctx.env,
|
|
56
|
-
}),
|
|
57
53
|
readAccountStatuses: async (lanes) => {
|
|
58
54
|
if (lanes.length === 0)
|
|
59
55
|
return [];
|
package/dist/commands/lanes.js
CHANGED
|
@@ -61,7 +61,7 @@ import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STA
|
|
|
61
61
|
import { laneStories, storyWarning, } from "../lib/lanes/stories.js";
|
|
62
62
|
import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
|
|
63
63
|
import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
|
|
64
|
-
import { doneKeepAliveVerdict, hasPendingKeepAliveWork, keepAliveNextCheckAtMs, keepAliveExclusion, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG,
|
|
64
|
+
import { doneKeepAliveVerdict, hasPendingKeepAliveWork, keepAliveNextCheckAtMs, keepAliveExclusion, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
|
|
65
65
|
import { questionBodyAbsPath, resolveQuestionCompanyWithFallback, resolveProjectQuestionCompany, } from "../lib/lanes/questions.js";
|
|
66
66
|
import { assertQuestionBodySecretSafe } from "../lib/lanes/questions.js";
|
|
67
67
|
import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
@@ -5159,17 +5159,11 @@ async function runShow(ctx, laneId, hasPendingQuestionForLane) {
|
|
|
5159
5159
|
const keepAliveState = resolveLaneKeepAlive(hqRoot, storedLane);
|
|
5160
5160
|
const keepAliveCompany = resolveLaneCompany(hqRoot, storedLane);
|
|
5161
5161
|
const now = ctx.now().getTime();
|
|
5162
|
-
const
|
|
5163
|
-
? await readLanesRegistryFlag(LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_FLAG, {
|
|
5164
|
-
company: keepAliveCompany.company ?? undefined,
|
|
5165
|
-
env: ctx.env,
|
|
5166
|
-
})
|
|
5167
|
-
: false;
|
|
5168
|
-
const hasPendingWork = storedLane.keep_alive_standing === true || nextCheckAtEnabled
|
|
5162
|
+
const hasPendingWork = storedLane.state === "done" && storedLane.keep_alive === true
|
|
5169
5163
|
? hasPendingKeepAliveWork(hqRoot, storedLane)
|
|
5170
5164
|
: false;
|
|
5171
5165
|
const doneStateVerdict = storedLane.state === "done" && storedLane.keep_alive === true
|
|
5172
|
-
? doneKeepAliveVerdict(storedLane, { decision: storedLane.last_envelope?.decision, story_complete: storedLane.story_complete, next_check_at: storedLane.last_envelope?.next_check_at }, storedLane.keep_alive_standing === true && hasPendingWork, {
|
|
5166
|
+
? doneKeepAliveVerdict(storedLane, { decision: storedLane.last_envelope?.decision, story_complete: storedLane.story_complete, next_check_at: storedLane.last_envelope?.next_check_at }, storedLane.keep_alive_standing === true && hasPendingWork, { hasPendingWork, nowMs: now })
|
|
5173
5167
|
: null;
|
|
5174
5168
|
let managerStatusTarget;
|
|
5175
5169
|
let managerStatusGateError;
|
|
@@ -5191,7 +5185,7 @@ async function runShow(ctx, laneId, hasPendingQuestionForLane) {
|
|
|
5191
5185
|
now: ctx.now().getTime(),
|
|
5192
5186
|
});
|
|
5193
5187
|
const laneIsSenior = allLanes.some((item) => item.senior.kind === "lane" && item.senior.id === laneId);
|
|
5194
|
-
const requestedNextCheckMs =
|
|
5188
|
+
const requestedNextCheckMs = storedLane.state === "done" && storedLane.keep_alive === true && !hasPendingWork
|
|
5195
5189
|
? keepAliveNextCheckAtMs(storedLane.last_envelope?.next_check_at, now, Date.parse(storedLane.last_envelope?.at ?? storedLane.updated_at))
|
|
5196
5190
|
: undefined;
|
|
5197
5191
|
const storedNextEligibleMs = Date.parse(storedLane.keep_alive_next_eligible_at ?? "");
|
package/dist/fast-scaffold.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type FastScaffoldScript = {
|
|
|
16
16
|
/** Path under `assets/scaffold/`, e.g. `core/scripts/foo.sh`. */
|
|
17
17
|
asset: string;
|
|
18
18
|
root?: "live";
|
|
19
|
+
/** Whether the bundled fast-route module closure needs Node's network timeout patch. */
|
|
20
|
+
requiresNodeNetworkCompatibility?: boolean;
|
|
19
21
|
} | {
|
|
20
22
|
/** Command path, joined with spaces, e.g. `policy frontmatter`. */
|
|
21
23
|
name: string;
|
|
@@ -38,6 +40,8 @@ export type FastScaffoldScript = {
|
|
|
38
40
|
shellHqRoot?: "none" | "live" | "reconcile-default";
|
|
39
41
|
/** Whether this handler consumes stdin. Shell companions inherit stdin directly. */
|
|
40
42
|
readsStdin?: boolean;
|
|
43
|
+
/** Whether the bundled fast-route module closure needs Node's network timeout patch. */
|
|
44
|
+
requiresNodeNetworkCompatibility?: boolean;
|
|
41
45
|
};
|
|
42
46
|
/** The relocated scripts that qualify for the entrypoint fast path. */
|
|
43
47
|
export declare const FAST_SCAFFOLD_SCRIPTS: readonly FastScaffoldScript[];
|
package/dist/fast-scaffold.js
CHANGED
|
@@ -115,6 +115,7 @@ export const FAST_SCAFFOLD_SCRIPTS = [
|
|
|
115
115
|
flagKey: "hq-core-native-fast-route",
|
|
116
116
|
familyFlagKey: "core-native-handoff",
|
|
117
117
|
asset: "core/scripts/worktree-gc.sh",
|
|
118
|
+
requiresNodeNetworkCompatibility: true,
|
|
118
119
|
root: "live",
|
|
119
120
|
shellCwd: "live",
|
|
120
121
|
shellHqRoot: "live",
|
|
@@ -127,6 +128,7 @@ export const FAST_SCAFFOLD_SCRIPTS = [
|
|
|
127
128
|
flagKey: "hq-core-native-fast-route",
|
|
128
129
|
familyFlagKey: "core-native-handoff",
|
|
129
130
|
asset: "core/scripts/clean-worktree-reconcile-handoff.sh",
|
|
131
|
+
requiresNodeNetworkCompatibility: true,
|
|
130
132
|
root: "cwd",
|
|
131
133
|
shellCwd: "caller",
|
|
132
134
|
shellHqRoot: "none",
|
|
@@ -139,6 +141,7 @@ export const FAST_SCAFFOLD_SCRIPTS = [
|
|
|
139
141
|
flagKey: "hq-core-native-fast-route",
|
|
140
142
|
familyFlagKey: "core-native-handoff",
|
|
141
143
|
asset: "core/scripts/resume-thread-lock.sh",
|
|
144
|
+
requiresNodeNetworkCompatibility: true,
|
|
142
145
|
root: "live",
|
|
143
146
|
shellCwd: "live",
|
|
144
147
|
shellHqRoot: "live",
|
|
@@ -151,6 +154,7 @@ export const FAST_SCAFFOLD_SCRIPTS = [
|
|
|
151
154
|
flagKey: "hq-core-native-fast-route",
|
|
152
155
|
familyFlagKey: "core-native-handoff",
|
|
153
156
|
asset: "core/scripts/verify-story-deliverables.sh",
|
|
157
|
+
requiresNodeNetworkCompatibility: true,
|
|
154
158
|
root: "live",
|
|
155
159
|
shellCwd: "live",
|
|
156
160
|
shellHqRoot: "live",
|
|
@@ -163,6 +167,7 @@ export const FAST_SCAFFOLD_SCRIPTS = [
|
|
|
163
167
|
flagKey: "hq-core-native-fast-route",
|
|
164
168
|
familyFlagKey: "core-native-jobs",
|
|
165
169
|
asset: `core/scripts/${name === "reconcile" ? "outpost-jobs-reconcile" : `jobs-${name}`}.sh`,
|
|
170
|
+
requiresNodeNetworkCompatibility: true,
|
|
166
171
|
root: name === "reconcile" ? "passthrough" : "cwd",
|
|
167
172
|
shellCwd: "caller",
|
|
168
173
|
shellHqRoot: name === "reconcile" ? "reconcile-default" : "none",
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import "./node-preflight.js";
|
|
3
|
-
import "./node-network-compat.js";
|
|
3
|
+
import "./node-network-compat-fast.js";
|
|
4
4
|
import { isVersionRequest } from "./version-request.js";
|
|
5
5
|
import { isFastCoreRequest } from "./fast-scaffold.js";
|
|
6
6
|
export declare const __test__: {
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// MUST be first: guard the Node version before any dependency that needs a
|
|
3
3
|
// Node 20+ API (e.g. util.styleText) or a newer native ABI is evaluated.
|
|
4
4
|
import "./node-preflight.js";
|
|
5
|
-
import "./node-network-compat.js";
|
|
5
|
+
import "./node-network-compat-fast.js";
|
|
6
6
|
import { isVersionRequest, wantsLabelledVersionReport } from "./version-request.js";
|
|
7
7
|
// Dependency-light: a pure parser + table, no command graph. Safe to load on
|
|
8
8
|
// every path (including --version) without reintroducing the heavy startup.
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { type WorkerOptions } from "node:worker_threads";
|
|
9
9
|
import { createFlagClient, type FlagClient, type FlagLookupOptions, type FlagSnapshot } from "@indigoai-us/hq-flags-client";
|
|
10
|
+
type FlagRegistrySentry = Pick<typeof import("@sentry/node"), "captureException" | "withScope">;
|
|
10
11
|
export { resolveProcessFlagCompanyContext } from "./process-flag-company-context.js";
|
|
11
12
|
export type { ProcessFlagCompanyContext } from "./process-flag-company-context.js";
|
|
12
13
|
export { FLAG_REGISTRY_ENDPOINT_ENV } from "./flag-registry-endpoint.js";
|
|
@@ -20,6 +21,7 @@ interface WorkerLike {
|
|
|
20
21
|
export interface FlagRegistryDependencies {
|
|
21
22
|
endpoint?: string;
|
|
22
23
|
createWorker?: (filename: string | URL, options: WorkerOptions) => WorkerLike;
|
|
24
|
+
sentry?: FlagRegistrySentry;
|
|
23
25
|
/** Test seam for exercising the no-source fail-open path. */
|
|
24
26
|
workerSource?: string;
|
|
25
27
|
captureWorkerFailure?: (error: Error, tags: Record<string, string>) => void;
|
|
@@ -6,12 +6,16 @@
|
|
|
6
6
|
* keep a short-lived, offline CLI alive until the client's network deadline.
|
|
7
7
|
*/
|
|
8
8
|
import { Worker } from "node:worker_threads";
|
|
9
|
-
import * as Sentry from "@sentry/node";
|
|
10
9
|
import { createFlagClient, isFlagSnapshot, readLocalOverride, resolveFlag, } from "@indigoai-us/hq-flags-client";
|
|
11
10
|
import { resolveFlagRegistryEndpoint, } from "./flag-registry-endpoint.js";
|
|
12
11
|
import { FLAG_REGISTRY_WORKER_SOURCE } from "./flag-registry-worker-source.generated.js";
|
|
13
12
|
import { resolveProcessFlagCompanyContext } from "./process-flag-company-context.js";
|
|
14
13
|
import { writeFlagSnapshotFile } from "./flag-snapshot-file.js";
|
|
14
|
+
function captureFlagRegistryException(error, tags, sentry) {
|
|
15
|
+
if (!sentry)
|
|
16
|
+
return;
|
|
17
|
+
sentry.captureException(error, { tags });
|
|
18
|
+
}
|
|
15
19
|
export { resolveProcessFlagCompanyContext } from "./process-flag-company-context.js";
|
|
16
20
|
export { FLAG_REGISTRY_ENDPOINT_ENV } from "./flag-registry-endpoint.js";
|
|
17
21
|
const COMPANY_UID_RE = /^cmp_[A-Za-z0-9]{3,128}$/;
|
|
@@ -58,6 +62,7 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
|
|
|
58
62
|
return processReader;
|
|
59
63
|
const endpoint = resolveFlagRegistryEndpoint(dependencies.endpoint, process.env);
|
|
60
64
|
const company = resolveProcessFlagCompanyContext();
|
|
65
|
+
const sentry = dependencies.sentry;
|
|
61
66
|
const companyUid = company.companyUid;
|
|
62
67
|
const companyIdentifiers = [companyUid, company.companySlug].filter((value) => Boolean(value));
|
|
63
68
|
processReader = processSnapshotReader(companyIdentifiers);
|
|
@@ -65,7 +70,7 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
|
|
|
65
70
|
const embeddedSource = dependencies.workerSource ?? FLAG_REGISTRY_WORKER_SOURCE;
|
|
66
71
|
if (embeddedSource.length === 0) {
|
|
67
72
|
(dependencies.captureWorkerFailure ?? ((error, tags) => {
|
|
68
|
-
|
|
73
|
+
captureFlagRegistryException(error, tags, sentry);
|
|
69
74
|
}))(new Error("Self-contained flag-registry worker source is unavailable"), {
|
|
70
75
|
component: "flag-registry-worker-startup",
|
|
71
76
|
worker: "flag-registry",
|
|
@@ -87,10 +92,14 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
|
|
|
87
92
|
// its process-wide captureWorkerErrors option is disabled. Scope only this
|
|
88
93
|
// worker so the reporting boundary can classify its startup errors without
|
|
89
94
|
// changing capture for unrelated workers.
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
const createWorker = dependencies.createWorker ??
|
|
96
|
+
((filename, options) => new Worker(filename, options));
|
|
97
|
+
const worker = sentry
|
|
98
|
+
? sentry.withScope((scope) => {
|
|
99
|
+
scope.setTag("worker", "flag-registry");
|
|
100
|
+
return createWorker(embeddedSource, workerOptions);
|
|
101
|
+
})
|
|
102
|
+
: createWorker(embeddedSource, workerOptions);
|
|
94
103
|
worker.on("message", (value) => {
|
|
95
104
|
if (isFlagSnapshot(value)) {
|
|
96
105
|
heldSnapshot = value;
|
|
@@ -99,12 +108,10 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
|
|
|
99
108
|
writeFlagSnapshotFile(companyUid, value.flags);
|
|
100
109
|
}
|
|
101
110
|
catch (error) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
},
|
|
107
|
-
});
|
|
111
|
+
captureFlagRegistryException(error instanceof Error ? error : new Error(String(error)), {
|
|
112
|
+
component: "flag-snapshot-cache-write",
|
|
113
|
+
company_uid_present: "true",
|
|
114
|
+
}, sentry);
|
|
108
115
|
}
|
|
109
116
|
}
|
|
110
117
|
}
|
|
@@ -154,7 +154,7 @@ function projectStories(hqRoot, relativePath, companySlug, projectId) {
|
|
|
154
154
|
status,
|
|
155
155
|
...(story.passes === true ? { passes: true } : {}),
|
|
156
156
|
...(briefPath ? { briefPath } : {}),
|
|
157
|
-
...(
|
|
157
|
+
...storyLaneIds(story),
|
|
158
158
|
...(typeof story.updated_at === "string" ? { updatedAt: story.updated_at } : {}),
|
|
159
159
|
...(typeof story.external_blocker === "string" ? { externalBlocker: story.external_blocker } : {}),
|
|
160
160
|
};
|
|
@@ -207,6 +207,18 @@ function parseOkrReports(status) {
|
|
|
207
207
|
return { at: value.at, kpis: parseLines(value.kpis, "KPI"), okrs: parseLines(value.okrs, "OKR") };
|
|
208
208
|
}).sort((a, b) => a.at.localeCompare(b.at));
|
|
209
209
|
}
|
|
210
|
+
// A story's current `lane` counts alongside its `lanes` history, so `lanes: []` with `lane: X` is assigned to X.
|
|
211
|
+
function storyLaneIds(story) {
|
|
212
|
+
const history = Array.isArray(story.lanes)
|
|
213
|
+
? story.lanes.map((lane) => typeof lane === "string" ? lane : lane.lane_id)
|
|
214
|
+
: undefined;
|
|
215
|
+
const current = typeof story.lane === "string" && story.lane.trim() ? story.lane : undefined;
|
|
216
|
+
if (history)
|
|
217
|
+
return { laneIds: current && !history.includes(current) ? [...history, current] : history };
|
|
218
|
+
if (current)
|
|
219
|
+
return { laneIds: [current] };
|
|
220
|
+
return story.lane === "" ? { laneIds: [] } : {};
|
|
221
|
+
}
|
|
210
222
|
function statusBlockers(status) {
|
|
211
223
|
const raw = status.blockers;
|
|
212
224
|
if (raw === undefined)
|
|
@@ -256,7 +268,9 @@ export function planLeadAutopilotWakes(input) {
|
|
|
256
268
|
const finishedMs = Date.parse(story.updatedAt);
|
|
257
269
|
if (!Number.isFinite(finishedMs) || now.getTime() - finishedMs < LEAD_AUTOPILOT_REVIEW_DELAY_MS)
|
|
258
270
|
return [];
|
|
259
|
-
|
|
271
|
+
// The lead's own lane is never a finished worker of itself.
|
|
272
|
+
return story.laneIds.filter((laneId) => laneId !== lead.lane_id && !knownChildIds.has(laneId))
|
|
273
|
+
.map((laneId) => ({ laneId, story, finishedAt: story.updatedAt }));
|
|
260
274
|
});
|
|
261
275
|
const overdueReviews = [...finishedReviews, ...missingReviews].sort((a, b) => a.laneId.localeCompare(b.laneId));
|
|
262
276
|
if (overdueReviews.length > 0) {
|
|
@@ -351,6 +365,9 @@ export function planLeadAutopilotWakes(input) {
|
|
|
351
365
|
if (story.status !== "in_progress")
|
|
352
366
|
return [];
|
|
353
367
|
const laneIds = story.laneIds ?? [];
|
|
368
|
+
// A story assigned to the lead itself is worked by the lead, which is the lane being woken.
|
|
369
|
+
if (laneIds.includes(lead.lane_id))
|
|
370
|
+
return [];
|
|
354
371
|
const live = laneIds.filter((laneId) => children.some((child) => child.lane_id === laneId && LIVE_STATES.has(child.state)));
|
|
355
372
|
if (live.length > 0)
|
|
356
373
|
return [];
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { type LaneRecord, type ManagerStatusGateHistoryEntry, type QuestionRecord } from "./types.js";
|
|
2
2
|
export declare const LEAD_LANE_KEEP_ALIVE_FLAG = "lanes.lead-keep-alive";
|
|
3
3
|
export declare const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
|
|
4
|
-
export declare const LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_FLAG = "lanes.keep-alive-next-check-at";
|
|
5
|
-
export declare const LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_DEFAULT_ENABLED = false;
|
|
6
4
|
export declare const LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_MAX_DELAY_MS: number;
|
|
7
5
|
export declare const LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG = "lanes.keep-alive-resume-blocked";
|
|
8
6
|
export declare const LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_DEFAULT_ENABLED = false;
|
|
@@ -30,6 +28,7 @@ export declare function queueLaneTerminalResume(current: LaneRecord, at: string,
|
|
|
30
28
|
allowResume?: boolean;
|
|
31
29
|
mergeExisting?: boolean;
|
|
32
30
|
allowBlockedResume?: boolean;
|
|
31
|
+
trigger?: "resume" | "watcher";
|
|
33
32
|
}): LaneTerminalResumeResult;
|
|
34
33
|
/** Persist a manager status miss and queue its one bounded follow-up turn. */
|
|
35
34
|
export declare function queueManagerStatusGateResume(hqRoot: string, laneId: string, entry: Omit<ManagerStatusGateHistoryEntry, "event" | "resume_queued" | "resume_skipped_reason">, resumeText: string): LaneRecord;
|
|
@@ -47,7 +46,6 @@ export type DoneKeepAliveVerdict = "eligible" | "standing_idle" | "story_complet
|
|
|
47
46
|
export declare function keepAliveNextCheckAtMs(value: unknown, nowMs: number, anchorMs?: number): number | undefined;
|
|
48
47
|
/** Shared done-state policy used by watch, reconcile, and the show command. */
|
|
49
48
|
export declare function doneKeepAliveVerdict(lane: Pick<LaneRecord, "keep_alive_standing">, envelope: KeepAliveDoneEnvelope, hasQueuedWork: boolean, options?: {
|
|
50
|
-
nextCheckAtEnabled?: boolean;
|
|
51
49
|
hasPendingWork?: boolean;
|
|
52
50
|
nowMs?: number;
|
|
53
51
|
nextCheckAnchorMs?: number;
|
|
@@ -6,8 +6,6 @@ import { appendCappedHistory, listQuestionIds, readQuestion, retryUpdateLane } f
|
|
|
6
6
|
import { isTerminalLaneState, isIsoDateTime, } from "./types.js";
|
|
7
7
|
export const LEAD_LANE_KEEP_ALIVE_FLAG = "lanes.lead-keep-alive";
|
|
8
8
|
export const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
|
|
9
|
-
export const LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_FLAG = "lanes.keep-alive-next-check-at";
|
|
10
|
-
export const LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_DEFAULT_ENABLED = false;
|
|
11
9
|
export const LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_MAX_DELAY_MS = 24 * 60 * 60 * 1000;
|
|
12
10
|
export const LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG = "lanes.keep-alive-resume-blocked";
|
|
13
11
|
export const LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_DEFAULT_ENABLED = false;
|
|
@@ -63,7 +61,7 @@ export function queueLaneTerminalResume(current, at, resumeText, options = {}) {
|
|
|
63
61
|
terminal_details: undefined,
|
|
64
62
|
pending_resume: {
|
|
65
63
|
session_id: sessionId,
|
|
66
|
-
trigger: "
|
|
64
|
+
trigger: options.trigger ?? "watcher",
|
|
67
65
|
resumed_from: current.state,
|
|
68
66
|
text: resumeText,
|
|
69
67
|
compact: true,
|
|
@@ -126,7 +124,7 @@ export function doneKeepAliveVerdict(lane, envelope, hasQueuedWork, options = {}
|
|
|
126
124
|
verdict = "story_complete";
|
|
127
125
|
else
|
|
128
126
|
verdict = hasQueuedWork ? "eligible" : "standing_idle";
|
|
129
|
-
if (verdict === "eligible" && options.
|
|
127
|
+
if (verdict === "eligible" && options.hasPendingWork !== true) {
|
|
130
128
|
const nowMs = options.nowMs ?? Date.now();
|
|
131
129
|
const anchorMs = options.nextCheckAnchorMs ?? nowMs;
|
|
132
130
|
const nextCheckAtMs = keepAliveNextCheckAtMs(envelope.next_check_at, nowMs, anchorMs);
|
|
@@ -25,7 +25,7 @@ import { readLanesRegistryFlag } from "./registry-flag.js";
|
|
|
25
25
|
import { resolveLaneCompany } from "./lane-company.js";
|
|
26
26
|
import { appendDropboxMessage } from "./dropbox.js";
|
|
27
27
|
import { DEFAULT_RUNAWAY_SEARCH_GUARD_CONFIG, listHostProcesses, runRunawayToolGuard, RUNAWAY_TOOL_GUARD_FLAG, } from "./runaway-tool-guard.js";
|
|
28
|
-
import { LEAD_LANE_KEEP_ALIVE_FLAG,
|
|
28
|
+
import { LEAD_LANE_KEEP_ALIVE_FLAG, LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG, LEAD_LANE_KEEP_ALIVE_IMMEDIATE_FIRST_RESUME_FLAG, LEAD_LANE_KEEP_ALIVE_FIRST_RESUME_SETTLE_MS, LEAD_LANE_KEEP_ALIVE_BLOCKED_GRACE_MS, LEAD_LANE_KEEP_ALIVE_BLOCKED_RESUME_TEXT, queueLaneTerminalResume, LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT, LEAD_LANE_KEEP_ALIVE_MAX_RESUMES, LEAD_LANE_KEEP_ALIVE_RESUME_TEXT, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, doneKeepAliveVerdict, hasPendingKeepAliveWork, keepAliveExclusion, loadRetiredLaneIds, leadLaneKeepAliveBackoffMs, undeliveredAnsweredQuestions, } from "./keep-alive.js";
|
|
29
29
|
import { providerSupportsSessionResume } from "./delivery.js";
|
|
30
30
|
import { applyDefaultQuotaHold, clearAccountQuotaAfterSuccessfulTurnBestEffort, readQuotaSignal, recordAccountQuota, } from "./quota.js";
|
|
31
31
|
import { answeredQuestionEntries, questionRecordRefusalCode, raiseEnvelopeQuestions, resolveQuestionCompanyWithFallback, } from "./questions.js";
|
|
@@ -1087,7 +1087,6 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
1087
1087
|
}
|
|
1088
1088
|
}));
|
|
1089
1089
|
const keepAliveFlagCompanies = new Map();
|
|
1090
|
-
const nextCheckAtFlagCompanyKeys = new Set();
|
|
1091
1090
|
for (const lane of lanes) {
|
|
1092
1091
|
if (!(lane.state === "done" || lane.state === "interrupted" || lane.state === "blocked" || lane.state === "killed" || lane.state === "cancelled" || lane.state === "failed") ||
|
|
1093
1092
|
(lane.keep_alive !== true && lane.worker_profile === undefined) ||
|
|
@@ -1098,26 +1097,25 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
1098
1097
|
!providerSupportsSessionResume(lane.provider) ||
|
|
1099
1098
|
!(lane.provider_session_id ?? lane.last_resume?.session_id))))
|
|
1100
1099
|
continue;
|
|
1101
|
-
let hasNextCheckAt = false;
|
|
1102
1100
|
if (lane.state === "done") {
|
|
1103
1101
|
const envelope = readLatestEnvelopeRecord(hqRoot, lane.lane_id)?.envelope;
|
|
1104
1102
|
if (!envelope || envelope.decision !== "done")
|
|
1105
1103
|
continue;
|
|
1106
1104
|
const hasQueuedWork = lane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, lane, questions);
|
|
1107
|
-
if (doneKeepAliveVerdict(lane, envelope, hasQueuedWork
|
|
1105
|
+
if (doneKeepAliveVerdict(lane, envelope, hasQueuedWork, {
|
|
1106
|
+
hasPendingWork: hasPendingKeepAliveWork(hqRoot, lane, questions),
|
|
1107
|
+
nowMs: now.getTime(),
|
|
1108
|
+
nextCheckAnchorMs: Date.parse(lane.last_envelope?.at ?? lane.updated_at),
|
|
1109
|
+
}) !== "eligible")
|
|
1108
1110
|
continue;
|
|
1109
|
-
hasNextCheckAt = envelope.next_check_at !== undefined;
|
|
1110
1111
|
}
|
|
1111
1112
|
const company = resolveLaneCompany(hqRoot, lane).company;
|
|
1112
1113
|
if (company === null)
|
|
1113
1114
|
continue;
|
|
1114
1115
|
const key = keepAliveCompanyKey(company);
|
|
1115
1116
|
keepAliveFlagCompanies.set(key, company);
|
|
1116
|
-
if (hasNextCheckAt)
|
|
1117
|
-
nextCheckAtFlagCompanyKeys.add(key);
|
|
1118
1117
|
}
|
|
1119
1118
|
const keepAliveFlagByCompany = new Map();
|
|
1120
|
-
const nextCheckAtFlagByCompany = new Map();
|
|
1121
1119
|
const blockedResumeFlagByCompany = new Map();
|
|
1122
1120
|
const immediateFirstResumeFlagByCompany = new Map();
|
|
1123
1121
|
const supersessionLineageAwareByCompany = new Map();
|
|
@@ -1130,11 +1128,8 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
1130
1128
|
lane.keep_alive === true &&
|
|
1131
1129
|
laneRunsHere(lane, startDeps.host) &&
|
|
1132
1130
|
keepAliveCompanyKey(resolveLaneCompany(hqRoot, lane).company) === key);
|
|
1133
|
-
const [keepAliveEnabled,
|
|
1131
|
+
const [keepAliveEnabled, lineageAware, blockedResumeEnabled, immediateFirstResumeEnabled] = await Promise.all([
|
|
1134
1132
|
readRegistryFlag(LEAD_LANE_KEEP_ALIVE_FLAG, options),
|
|
1135
|
-
nextCheckAtFlagCompanyKeys.has(key)
|
|
1136
|
-
? readRegistryFlag(LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_FLAG, options)
|
|
1137
|
-
: Promise.resolve(false),
|
|
1138
1133
|
readRegistryFlag(LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, options),
|
|
1139
1134
|
hasBlockedKeepAliveLane
|
|
1140
1135
|
? readRegistryFlag(LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG, options)
|
|
@@ -1142,7 +1137,6 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
1142
1137
|
readRegistryFlag(LEAD_LANE_KEEP_ALIVE_IMMEDIATE_FIRST_RESUME_FLAG, options),
|
|
1143
1138
|
]);
|
|
1144
1139
|
keepAliveFlagByCompany.set(key, keepAliveEnabled);
|
|
1145
|
-
nextCheckAtFlagByCompany.set(key, nextCheckAtEnabled);
|
|
1146
1140
|
blockedResumeFlagByCompany.set(key, blockedResumeEnabled);
|
|
1147
1141
|
immediateFirstResumeFlagByCompany.set(key, immediateFirstResumeEnabled);
|
|
1148
1142
|
supersessionLineageAwareByCompany.set(key, lineageAware);
|
|
@@ -1165,7 +1159,7 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
1165
1159
|
const freshLanes = loadLaneRecords(hqRoot, summary);
|
|
1166
1160
|
const freshQuestions = loadQuestionRecords(hqRoot, summary);
|
|
1167
1161
|
await closeInterruptedDoneStoryWorkers(hqRoot, freshLanes, freshQuestions, summary, admissionDeps, now, storyCloseEnabledByCompany);
|
|
1168
|
-
await resumeKeepAlive(hqRoot, freshLanes, freshQuestions, summary, admissionDeps, now, keepAliveFlagByCompany, supersessionLineageAwareByCompany, blockedResumeFlagByCompany,
|
|
1162
|
+
await resumeKeepAlive(hqRoot, freshLanes, freshQuestions, summary, admissionDeps, now, keepAliveFlagByCompany, supersessionLineageAwareByCompany, blockedResumeFlagByCompany, immediateFirstResumeFlagByCompany);
|
|
1169
1163
|
const drainLanes = loadLaneRecords(hqRoot, summary);
|
|
1170
1164
|
await admitQueued(hqRoot, drainLanes, summary, admissionDeps, now);
|
|
1171
1165
|
await resumeAwaiting(hqRoot, drainLanes, freshQuestions, summary, admissionDeps);
|
|
@@ -2188,7 +2182,7 @@ async function resumeBlockedKeepAlive(hqRoot, lane, questions, summary, deps, no
|
|
|
2188
2182
|
current = afterPreparation;
|
|
2189
2183
|
}
|
|
2190
2184
|
const expectedVersion = current.version;
|
|
2191
|
-
const result = queueLaneTerminalResume(current, now.toISOString(), LEAD_LANE_KEEP_ALIVE_BLOCKED_RESUME_TEXT, { allowBlockedResume: true });
|
|
2185
|
+
const result = queueLaneTerminalResume(current, now.toISOString(), LEAD_LANE_KEEP_ALIVE_BLOCKED_RESUME_TEXT, { allowBlockedResume: true, trigger: "resume" });
|
|
2192
2186
|
if (!result.resumeQueued) {
|
|
2193
2187
|
if (result.resumeSkippedReason !== undefined) {
|
|
2194
2188
|
summary.skipped.push({ kind: "lane", id: lane.lane_id, why: "keep_alive_" + result.resumeSkippedReason });
|
|
@@ -2244,7 +2238,7 @@ function appendBlockedKeepAliveHistory(hqRoot, lane, terminalAt, at, reason, dep
|
|
|
2244
2238
|
}),
|
|
2245
2239
|
}), false);
|
|
2246
2240
|
}
|
|
2247
|
-
async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCompany, supersessionLineageAwareByCompany, blockedResumeFlagByCompany,
|
|
2241
|
+
async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCompany, supersessionLineageAwareByCompany, blockedResumeFlagByCompany, immediateFirstResumeFlagByCompany) {
|
|
2248
2242
|
const nowMs = now.getTime();
|
|
2249
2243
|
const nowIso = now.toISOString();
|
|
2250
2244
|
const config = loadLanesConfig(hqRoot);
|
|
@@ -2291,13 +2285,9 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2291
2285
|
const envelope = readLatestEnvelopeRecord(hqRoot, lane.lane_id)?.envelope;
|
|
2292
2286
|
if (!envelope || envelope.decision !== "done")
|
|
2293
2287
|
continue;
|
|
2294
|
-
const
|
|
2295
|
-
const hasPendingWork = lane.keep_alive_standing === true || nextCheckAtEnabled
|
|
2296
|
-
? hasPendingKeepAliveWork(hqRoot, lane, questions)
|
|
2297
|
-
: false;
|
|
2288
|
+
const hasPendingWork = hasPendingKeepAliveWork(hqRoot, lane, questions);
|
|
2298
2289
|
const hasQueuedWork = lane.keep_alive_standing === true && hasPendingWork;
|
|
2299
2290
|
const verdict = doneKeepAliveVerdict(lane, envelope, hasQueuedWork, {
|
|
2300
|
-
nextCheckAtEnabled,
|
|
2301
2291
|
hasPendingWork,
|
|
2302
2292
|
nowMs,
|
|
2303
2293
|
nextCheckAnchorMs: Date.parse(lane.last_envelope?.at ?? lane.updated_at),
|
|
@@ -2415,13 +2405,9 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2415
2405
|
const latest = readLatestEnvelopeRecord(hqRoot, lane.lane_id);
|
|
2416
2406
|
if (!latest || latest.envelope.decision !== "done")
|
|
2417
2407
|
continue;
|
|
2418
|
-
const
|
|
2419
|
-
const hasPendingWork = lane.keep_alive_standing === true || nextCheckAtEnabled
|
|
2420
|
-
? hasPendingKeepAliveWork(hqRoot, lane, questions)
|
|
2421
|
-
: false;
|
|
2408
|
+
const hasPendingWork = hasPendingKeepAliveWork(hqRoot, lane, questions);
|
|
2422
2409
|
const hasQueuedWork = lane.keep_alive_standing === true && hasPendingWork;
|
|
2423
2410
|
const verdict = doneKeepAliveVerdict(lane, latest.envelope, hasQueuedWork, {
|
|
2424
|
-
nextCheckAtEnabled,
|
|
2425
2411
|
hasPendingWork,
|
|
2426
2412
|
nowMs,
|
|
2427
2413
|
nextCheckAnchorMs: Date.parse(lane.last_envelope?.at ?? lane.updated_at),
|
|
@@ -2507,13 +2493,9 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2507
2493
|
const currentEnvelope = readLatestEnvelopeRecord(hqRoot, current.lane_id);
|
|
2508
2494
|
if (!currentEnvelope || currentEnvelope.envelope.decision !== "done")
|
|
2509
2495
|
continue;
|
|
2510
|
-
const
|
|
2511
|
-
const hasPendingWork = current.keep_alive_standing === true || nextCheckAtEnabled
|
|
2512
|
-
? hasPendingKeepAliveWork(hqRoot, current, questions)
|
|
2513
|
-
: false;
|
|
2496
|
+
const hasPendingWork = hasPendingKeepAliveWork(hqRoot, current, questions);
|
|
2514
2497
|
const hasQueuedWork = current.keep_alive_standing === true && hasPendingWork;
|
|
2515
2498
|
const verdict = doneKeepAliveVerdict(current, currentEnvelope.envelope, hasQueuedWork, {
|
|
2516
|
-
nextCheckAtEnabled,
|
|
2517
2499
|
hasPendingWork,
|
|
2518
2500
|
nowMs,
|
|
2519
2501
|
nextCheckAnchorMs: Date.parse(current.last_envelope?.at ?? current.updated_at),
|
|
@@ -2794,12 +2776,8 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2794
2776
|
const envelope = readLatestEnvelopeRecord(hqRoot, latest.lane_id);
|
|
2795
2777
|
if (!envelope || envelope.envelope.decision !== "done")
|
|
2796
2778
|
continue;
|
|
2797
|
-
const
|
|
2798
|
-
const hasPendingWork = latest.keep_alive_standing === true || nextCheckAtEnabled
|
|
2799
|
-
? hasPendingKeepAliveWork(hqRoot, latest, questions)
|
|
2800
|
-
: false;
|
|
2779
|
+
const hasPendingWork = hasPendingKeepAliveWork(hqRoot, latest, questions);
|
|
2801
2780
|
const finalDoneVerdict = doneKeepAliveVerdict(latest, envelope.envelope, latest.keep_alive_standing === true && hasPendingWork, {
|
|
2802
|
-
nextCheckAtEnabled,
|
|
2803
2781
|
hasPendingWork,
|
|
2804
2782
|
nowMs,
|
|
2805
2783
|
nextCheckAnchorMs: Date.parse(latest.last_envelope?.at ?? latest.updated_at),
|
|
@@ -198,6 +198,8 @@ export declare function resumePrompt(input: {
|
|
|
198
198
|
answers?: AnsweredQuestionLine[];
|
|
199
199
|
/** `hq lanes resume`: the state the operator resumed the lane from. */
|
|
200
200
|
resumedFrom?: string;
|
|
201
|
+
/** Automatic continuation initiated by the lane watcher. */
|
|
202
|
+
resumeSource?: "watcher";
|
|
201
203
|
/** Exact validator errors recorded after the prior envelope repair failed. */
|
|
202
204
|
envelopeRepairErrors?: {
|
|
203
205
|
first_error: string;
|
package/dist/lib/lanes/spawn.js
CHANGED
|
@@ -372,7 +372,18 @@ function pendingResumeForRetry(hqRoot, lane, sessionId, at) {
|
|
|
372
372
|
if (last.trigger === "answered_questions") {
|
|
373
373
|
return { session_id: sessionId, trigger: "answered_questions", at, ...compact, ...keepAlive };
|
|
374
374
|
}
|
|
375
|
-
if (last.trigger === "
|
|
375
|
+
if (last.trigger === "watcher") {
|
|
376
|
+
return {
|
|
377
|
+
session_id: sessionId,
|
|
378
|
+
trigger: "watcher",
|
|
379
|
+
resumed_from: last.resumed_from ?? "interrupted",
|
|
380
|
+
...(last.text !== undefined ? { text: last.text } : {}),
|
|
381
|
+
at,
|
|
382
|
+
...compact,
|
|
383
|
+
...keepAlive,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
if (last.trigger === "message" || last.trigger === "monitor_event" || last.trigger === "system_notice") {
|
|
376
387
|
if (last.message_id === undefined) {
|
|
377
388
|
throw new LanesError("account_retry_context_missing", `The message id needed to resume lane ${lane.lane_id} is missing`, { lane_id: lane.lane_id, trigger: last.trigger });
|
|
378
389
|
}
|
|
@@ -382,9 +393,10 @@ function pendingResumeForRetry(hqRoot, lane, sessionId, at) {
|
|
|
382
393
|
}
|
|
383
394
|
return {
|
|
384
395
|
session_id: sessionId,
|
|
385
|
-
trigger: "message",
|
|
396
|
+
trigger: last.trigger === "message" && last.monitor_event === true ? "monitor_event" : last.trigger,
|
|
386
397
|
text,
|
|
387
398
|
message_id: last.message_id,
|
|
399
|
+
...(last.monitor_event === true || last.trigger === "monitor_event" ? { monitor_event: true } : {}),
|
|
388
400
|
at,
|
|
389
401
|
...compact,
|
|
390
402
|
...keepAlive,
|
|
@@ -1071,7 +1083,7 @@ export function readAnsweredQuestionLines(answersPath) {
|
|
|
1071
1083
|
export function resumePrompt(input) {
|
|
1072
1084
|
const parts = [];
|
|
1073
1085
|
if (input.resumedFrom !== undefined) {
|
|
1074
|
-
parts.push(
|
|
1086
|
+
parts.push(`${input.resumeSource === "watcher" ? "The lane watcher" : "The operator"} resumed this lane from state ${input.resumedFrom}. ` +
|
|
1075
1087
|
(input.resumedFrom === "interrupted"
|
|
1076
1088
|
? "The previous run ended without writing its envelope; continue the work from where it stopped."
|
|
1077
1089
|
: "Continue the work from where the previous run stopped."));
|
|
@@ -1743,7 +1755,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1743
1755
|
const profileChanged = profile !== undefined && existing.worker_profile_hash !== profile.contentHash;
|
|
1744
1756
|
const config = loadLanesConfig(hqRoot);
|
|
1745
1757
|
const repairRecordedAccount = plan.mode === "resume" &&
|
|
1746
|
-
(trigger.kind === "resume" || pendingResume?.trigger === "resume");
|
|
1758
|
+
(trigger.kind === "resume" || pendingResume?.trigger === "resume" || pendingResume?.trigger === "watcher");
|
|
1747
1759
|
const recordedModel = repairRecordedAccount && existing.model_source === "account"
|
|
1748
1760
|
? existing.model
|
|
1749
1761
|
: undefined;
|
|
@@ -1919,6 +1931,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1919
1931
|
? pendingResume.text
|
|
1920
1932
|
: undefined,
|
|
1921
1933
|
resumedFrom: trigger.kind === "resume" ? trigger.from : pendingResume?.resumed_from,
|
|
1934
|
+
resumeSource: pendingResume?.trigger === "watcher" ? "watcher" : undefined,
|
|
1922
1935
|
answersPath: answeredLines !== undefined ? answersPath : undefined,
|
|
1923
1936
|
answers: answeredLines,
|
|
1924
1937
|
envelopeRepairErrors: lane.terminal_reason === ENVELOPE_INVALID_REASON &&
|
|
@@ -2019,14 +2032,24 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
2019
2032
|
const nowIso = (deps.now ?? (() => new Date()))().toISOString();
|
|
2020
2033
|
const resumeHistoryTrigger = pendingResume?.keep_alive === true
|
|
2021
2034
|
? "keep_alive"
|
|
2022
|
-
: pendingResume?.trigger === "
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2035
|
+
: pendingResume?.trigger === "monitor_event" || pendingResume?.monitor_event === true ||
|
|
2036
|
+
(trigger.kind === "message" && trigger.monitor_event === true)
|
|
2037
|
+
? "monitor_event"
|
|
2038
|
+
: pendingResume?.trigger === "system_notice" ||
|
|
2039
|
+
(trigger.kind === "message" && trigger.system_notice === true)
|
|
2040
|
+
? "system_notice"
|
|
2041
|
+
: pendingResume?.trigger === "watcher"
|
|
2042
|
+
? "watcher"
|
|
2043
|
+
: pendingResume?.trigger === "message" || trigger.kind === "message"
|
|
2044
|
+
? "message"
|
|
2045
|
+
: pendingResume?.trigger === "resume" || trigger.kind === "resume"
|
|
2046
|
+
? "resume_cli"
|
|
2047
|
+
: "reconcile";
|
|
2027
2048
|
const resumeHistoryCaller = pendingResume?.caller ||
|
|
2028
|
-
(typeof env.HQ_LANE_ID === "string" ? env.HQ_LANE_ID.trim() : "") ||
|
|
2029
|
-
(trigger
|
|
2049
|
+
(pendingResume?.trigger !== "watcher" && typeof env.HQ_LANE_ID === "string" ? env.HQ_LANE_ID.trim() : "") ||
|
|
2050
|
+
(pendingResume?.trigger === "message" || pendingResume?.trigger === "monitor_event" ||
|
|
2051
|
+
pendingResume?.trigger === "system_notice" || trigger.kind === "answered_questions" ||
|
|
2052
|
+
trigger.kind === "resume" || trigger.kind === "message"
|
|
2030
2053
|
? "user"
|
|
2031
2054
|
: undefined);
|
|
2032
2055
|
let statusGateHeartbeat;
|
|
@@ -2159,7 +2182,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
2159
2182
|
session_id: resume.session_id,
|
|
2160
2183
|
at: nowIso,
|
|
2161
2184
|
account: resolved.id,
|
|
2162
|
-
trigger: trigger.kind,
|
|
2185
|
+
trigger: trigger.kind === "message" && trigger.system_notice === true ? "system_notice" : trigger.kind,
|
|
2163
2186
|
...(trigger.kind === "message"
|
|
2164
2187
|
? {
|
|
2165
2188
|
message_id: trigger.message_id,
|
|
@@ -766,7 +766,7 @@ export interface FailedResumeHistoryEntry {
|
|
|
766
766
|
export interface PendingResume {
|
|
767
767
|
/** Fresh answered-question launches need no previous session id. */
|
|
768
768
|
session_id?: string;
|
|
769
|
-
trigger: "answered_questions" | "message" | "resume";
|
|
769
|
+
trigger: "answered_questions" | "message" | "monitor_event" | "system_notice" | "resume" | "watcher";
|
|
770
770
|
/** True when this queued resume was initiated automatically by keep-alive. */
|
|
771
771
|
keep_alive?: true;
|
|
772
772
|
resumed_from?: "interrupted" | "done" | "blocked" | "failed" | "awaiting_input";
|
|
@@ -797,7 +797,7 @@ export interface LaneResumeRecord {
|
|
|
797
797
|
at: string;
|
|
798
798
|
/** Account the resumed run was bound to. */
|
|
799
799
|
account?: string;
|
|
800
|
-
trigger: "answered_questions" | "message" | "resume";
|
|
800
|
+
trigger: "answered_questions" | "message" | "monitor_event" | "system_notice" | "resume" | "watcher";
|
|
801
801
|
/** True when this was an automatic keep-alive resume. */
|
|
802
802
|
keep_alive?: true;
|
|
803
803
|
message_id?: string;
|
|
@@ -23,7 +23,6 @@ export interface LaneWatchDeps {
|
|
|
23
23
|
readQuestion?: (hqRoot: string, questionId: string) => QuestionRecord | null;
|
|
24
24
|
readAccountStatuses?: (lanes: readonly LaneRecord[]) => Promise<readonly LaneWatchAccountStatus[]>;
|
|
25
25
|
keepAliveEnabled?: (lane: LaneRecord) => boolean | Promise<boolean>;
|
|
26
|
-
keepAliveNextCheckAtEnabled?: (lane: LaneRecord) => boolean | Promise<boolean>;
|
|
27
26
|
sleep?: (ms: number) => Promise<void>;
|
|
28
27
|
intervalMs?: number;
|
|
29
28
|
maxOutputLines?: number;
|
package/dist/lib/lanes/watch.js
CHANGED
|
@@ -32,7 +32,7 @@ function validateLaneIds(laneIds) {
|
|
|
32
32
|
}
|
|
33
33
|
return unique;
|
|
34
34
|
}
|
|
35
|
-
function canResumeKeepAliveLane(hqRoot, lane,
|
|
35
|
+
function canResumeKeepAliveLane(hqRoot, lane, nowMs = Date.now()) {
|
|
36
36
|
if (lane.keep_alive !== true ||
|
|
37
37
|
lane.keep_alive_opted_in_at === undefined ||
|
|
38
38
|
!Number.isFinite(Date.parse(lane.keep_alive_opted_in_at)) ||
|
|
@@ -49,10 +49,8 @@ function canResumeKeepAliveLane(hqRoot, lane, nextCheckAtEnabled = false, nowMs
|
|
|
49
49
|
return lane.terminal_reason === "exited_with_partial_output";
|
|
50
50
|
}
|
|
51
51
|
if (lane.state === "done") {
|
|
52
|
-
const hasPendingWork = lane
|
|
53
|
-
|
|
54
|
-
: false;
|
|
55
|
-
const nextCheckAtMs = nextCheckAtEnabled && !hasPendingWork
|
|
52
|
+
const hasPendingWork = hasPendingKeepAliveWork(hqRoot, lane);
|
|
53
|
+
const nextCheckAtMs = !hasPendingWork
|
|
56
54
|
? keepAliveNextCheckAtMs(lane.last_envelope?.next_check_at, nowMs, Date.parse(lane.last_envelope?.at ?? lane.updated_at))
|
|
57
55
|
: undefined;
|
|
58
56
|
if (nextCheckAtMs !== undefined && nowMs < nextCheckAtMs)
|
|
@@ -143,10 +141,7 @@ export async function runLaneWatch(hqRoot, laneIds, deps = {}) {
|
|
|
143
141
|
continue;
|
|
144
142
|
if (!await deps.keepAliveEnabled(record))
|
|
145
143
|
continue;
|
|
146
|
-
|
|
147
|
-
? false
|
|
148
|
-
: await deps.keepAliveNextCheckAtEnabled(record);
|
|
149
|
-
if (canResumeKeepAliveLane(hqRoot, record, nextCheckAtEnabled))
|
|
144
|
+
if (canResumeKeepAliveLane(hqRoot, record))
|
|
150
145
|
keepAliveWaiting.add(record.lane_id);
|
|
151
146
|
}
|
|
152
147
|
}
|
package/dist/main.js
CHANGED
|
@@ -201,7 +201,7 @@ export async function runCli() {
|
|
|
201
201
|
const isAuthLogin = process.argv[2] === "auth" && process.argv[3] === "login";
|
|
202
202
|
if (!isAuthLogin) {
|
|
203
203
|
runGlobalNetworkLifecycle(process.argv, () => {
|
|
204
|
-
void kickFlagRegistryReadiness();
|
|
204
|
+
void kickFlagRegistryReadiness({ sentry: Sentry });
|
|
205
205
|
});
|
|
206
206
|
}
|
|
207
207
|
// Set when a self-update re-exec'd this command on a newer CLI: the child
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Node network defaults shared by the full graph and the selective fast path. */
|
|
2
|
+
export declare const HQ_NETWORK_FAMILY_ATTEMPT_TIMEOUT_MS = 1000;
|
|
3
|
+
export interface NodeNetworkCompatibilityOptions {
|
|
4
|
+
nodeOptions?: string;
|
|
5
|
+
execArgv?: readonly string[];
|
|
6
|
+
/** Inject the net timeout setter (tests). Pass null to simulate a runtime without it. */
|
|
7
|
+
setAttemptTimeout?: ((milliseconds: number) => void) | null;
|
|
8
|
+
}
|
|
9
|
+
export declare function configureNodeNetworkCompatibility(options?: NodeNetworkCompatibilityOptions): boolean;
|
|
10
|
+
/** Apply at most once when both entrypoint routing layers import this module. */
|
|
11
|
+
export declare function configureNodeNetworkCompatibilityOnce(): boolean;
|
|
12
|
+
//# sourceMappingURL=node-network-compat-core.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Node network defaults shared by the full graph and the selective fast path. */
|
|
2
|
+
import * as nodeNet from "node:net";
|
|
3
|
+
export const HQ_NETWORK_FAMILY_ATTEMPT_TIMEOUT_MS = 1000;
|
|
4
|
+
const ATTEMPT_TIMEOUT_OPTION = "--network-family-autoselection-attempt-timeout";
|
|
5
|
+
function hasExplicitAttemptTimeout(nodeOptions, execArgv) {
|
|
6
|
+
const optionPattern = new RegExp("(?:^|\\s)" + ATTEMPT_TIMEOUT_OPTION + "(?:=|\\s|$)");
|
|
7
|
+
return (optionPattern.test(nodeOptions) ||
|
|
8
|
+
execArgv.some((argument) => argument === ATTEMPT_TIMEOUT_OPTION ||
|
|
9
|
+
argument.startsWith(ATTEMPT_TIMEOUT_OPTION + "=")));
|
|
10
|
+
}
|
|
11
|
+
function resolveSetAttemptTimeout(override) {
|
|
12
|
+
if (override === null)
|
|
13
|
+
return undefined;
|
|
14
|
+
if (override)
|
|
15
|
+
return override;
|
|
16
|
+
const fn = nodeNet.setDefaultAutoSelectFamilyAttemptTimeout;
|
|
17
|
+
return typeof fn === "function" ? fn.bind(nodeNet) : undefined;
|
|
18
|
+
}
|
|
19
|
+
export function configureNodeNetworkCompatibility(options = {}) {
|
|
20
|
+
const nodeOptions = options.nodeOptions ?? process.env.NODE_OPTIONS ?? "";
|
|
21
|
+
const execArgv = options.execArgv ?? process.execArgv;
|
|
22
|
+
if (hasExplicitAttemptTimeout(nodeOptions, execArgv)) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const setAttemptTimeout = resolveSetAttemptTimeout(options.setAttemptTimeout);
|
|
26
|
+
if (!setAttemptTimeout) {
|
|
27
|
+
// Runtime lacks the API; the Node preflight should already have exited.
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
setAttemptTimeout(HQ_NETWORK_FAMILY_ATTEMPT_TIMEOUT_MS);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
let configurationAttempted = false;
|
|
34
|
+
/** Apply at most once when both entrypoint routing layers import this module. */
|
|
35
|
+
export function configureNodeNetworkCompatibilityOnce() {
|
|
36
|
+
if (configurationAttempted)
|
|
37
|
+
return false;
|
|
38
|
+
configurationAttempted = true;
|
|
39
|
+
return configureNodeNetworkCompatibility();
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=node-network-compat-core.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Selective Node network setup for the entrypoint's fast routes. */
|
|
2
|
+
/**
|
|
3
|
+
* Full-graph invocations keep the historical unconditional network patch.
|
|
4
|
+
* Fast routes opt in only when their direct implementation uses Node networking.
|
|
5
|
+
*/
|
|
6
|
+
export declare function shouldConfigureNodeNetworkCompatibility(argv: readonly string[]): boolean;
|
|
7
|
+
//# sourceMappingURL=node-network-compat-fast.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Selective Node network setup for the entrypoint's fast routes. */
|
|
2
|
+
import { parseFastCore, parseSecretsEnvFastRequest } from "./fast-scaffold.js";
|
|
3
|
+
import { isVersionRequest, wantsLabelledVersionReport } from "./version-request.js";
|
|
4
|
+
/**
|
|
5
|
+
* Full-graph invocations keep the historical unconditional network patch.
|
|
6
|
+
* Fast routes opt in only when their direct implementation uses Node networking.
|
|
7
|
+
*/
|
|
8
|
+
export function shouldConfigureNodeNetworkCompatibility(argv) {
|
|
9
|
+
if (isVersionRequest(argv))
|
|
10
|
+
return wantsLabelledVersionReport(argv);
|
|
11
|
+
const fastCore = parseFastCore(argv);
|
|
12
|
+
if (fastCore)
|
|
13
|
+
return fastCore.entry.requiresNodeNetworkCompatibility === true;
|
|
14
|
+
// The lightweight secrets route still makes vault API requests.
|
|
15
|
+
if (parseSecretsEnvFastRequest(argv))
|
|
16
|
+
return true;
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
if (shouldConfigureNodeNetworkCompatibility(process.argv)) {
|
|
20
|
+
const { configureNodeNetworkCompatibilityOnce } = await import("./node-network-compat-core.js");
|
|
21
|
+
configureNodeNetworkCompatibilityOnce();
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=node-network-compat-fast.js.map
|
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* The compatibility timeout remains the default for HQ CLI entry points.
|
|
5
|
-
*
|
|
6
|
-
* Keep this module safe to import immediately after the Node version preflight.
|
|
7
|
-
*/
|
|
8
|
-
export declare const HQ_NETWORK_FAMILY_ATTEMPT_TIMEOUT_MS = 1000;
|
|
9
|
-
export interface NodeNetworkCompatibilityOptions {
|
|
10
|
-
nodeOptions?: string;
|
|
11
|
-
execArgv?: readonly string[];
|
|
12
|
-
/**
|
|
13
|
-
* Inject the net timeout setter (tests). Pass null to simulate a runtime
|
|
14
|
-
* that lacks setDefaultAutoSelectFamilyAttemptTimeout.
|
|
15
|
-
*/
|
|
16
|
-
setAttemptTimeout?: ((milliseconds: number) => void) | null;
|
|
17
|
-
}
|
|
18
|
-
export declare function configureNodeNetworkCompatibility(options?: NodeNetworkCompatibilityOptions): boolean;
|
|
1
|
+
/** Unconditional compatibility setup for the full CLI command graph. */
|
|
2
|
+
export { configureNodeNetworkCompatibility, HQ_NETWORK_FAMILY_ATTEMPT_TIMEOUT_MS, type NodeNetworkCompatibilityOptions, } from "./node-network-compat-core.js";
|
|
19
3
|
//# sourceMappingURL=node-network-compat.d.ts.map
|
|
@@ -1,40 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
*
|
|
6
|
-
* Keep this module safe to import immediately after the Node version preflight.
|
|
7
|
-
*/
|
|
8
|
-
import * as nodeNet from "node:net";
|
|
9
|
-
export const HQ_NETWORK_FAMILY_ATTEMPT_TIMEOUT_MS = 1000;
|
|
10
|
-
const ATTEMPT_TIMEOUT_OPTION = "--network-family-autoselection-attempt-timeout";
|
|
11
|
-
function hasExplicitAttemptTimeout(nodeOptions, execArgv) {
|
|
12
|
-
const optionPattern = new RegExp("(?:^|\\s)" + ATTEMPT_TIMEOUT_OPTION + "(?:=|\\s|$)");
|
|
13
|
-
return (optionPattern.test(nodeOptions) ||
|
|
14
|
-
execArgv.some((argument) => argument === ATTEMPT_TIMEOUT_OPTION ||
|
|
15
|
-
argument.startsWith(ATTEMPT_TIMEOUT_OPTION + "=")));
|
|
16
|
-
}
|
|
17
|
-
function resolveSetAttemptTimeout(override) {
|
|
18
|
-
if (override === null)
|
|
19
|
-
return undefined;
|
|
20
|
-
if (override)
|
|
21
|
-
return override;
|
|
22
|
-
const fn = nodeNet.setDefaultAutoSelectFamilyAttemptTimeout;
|
|
23
|
-
return typeof fn === "function" ? fn.bind(nodeNet) : undefined;
|
|
24
|
-
}
|
|
25
|
-
export function configureNodeNetworkCompatibility(options = {}) {
|
|
26
|
-
const nodeOptions = options.nodeOptions ?? process.env.NODE_OPTIONS ?? "";
|
|
27
|
-
const execArgv = options.execArgv ?? process.execArgv;
|
|
28
|
-
if (hasExplicitAttemptTimeout(nodeOptions, execArgv)) {
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
const setAttemptTimeout = resolveSetAttemptTimeout(options.setAttemptTimeout);
|
|
32
|
-
if (!setAttemptTimeout) {
|
|
33
|
-
// Runtime lacks the API; the Node preflight should already have exited.
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
setAttemptTimeout(HQ_NETWORK_FAMILY_ATTEMPT_TIMEOUT_MS);
|
|
37
|
-
return true;
|
|
38
|
-
}
|
|
39
|
-
configureNodeNetworkCompatibility();
|
|
1
|
+
/** Unconditional compatibility setup for the full CLI command graph. */
|
|
2
|
+
import { configureNodeNetworkCompatibilityOnce } from "./node-network-compat-core.js";
|
|
3
|
+
export { configureNodeNetworkCompatibility, HQ_NETWORK_FAMILY_ATTEMPT_TIMEOUT_MS, } from "./node-network-compat-core.js";
|
|
4
|
+
configureNodeNetworkCompatibilityOnce();
|
|
40
5
|
//# sourceMappingURL=node-network-compat.js.map
|
package/dist/node-preflight.d.ts
CHANGED
|
@@ -23,6 +23,12 @@ export interface NodeVersionCheck {
|
|
|
23
23
|
major: number;
|
|
24
24
|
message?: string;
|
|
25
25
|
}
|
|
26
|
+
export interface NodePreflightOptions {
|
|
27
|
+
versionString?: string;
|
|
28
|
+
skip?: string;
|
|
29
|
+
stderr?: Pick<NodeJS.WriteStream, "write">;
|
|
30
|
+
exit?: (code: number) => void;
|
|
31
|
+
}
|
|
26
32
|
/**
|
|
27
33
|
* Pure check: is the given Node version string (e.g. "18.19.0") supported?
|
|
28
34
|
* Defaults to the running runtime's version. An unparseable version is treated
|
|
@@ -35,5 +41,5 @@ export declare function checkNodeVersion(versionString?: string): NodeVersionChe
|
|
|
35
41
|
* `HQ_SKIP_NODE_PREFLIGHT=1` to bypass (used by the test runner, which already
|
|
36
42
|
* runs on a supported Node).
|
|
37
43
|
*/
|
|
38
|
-
export declare function enforceNodeVersion(): void;
|
|
44
|
+
export declare function enforceNodeVersion(options?: NodePreflightOptions): void;
|
|
39
45
|
//# sourceMappingURL=node-preflight.d.ts.map
|
package/dist/node-preflight.js
CHANGED
|
@@ -39,13 +39,13 @@ export function checkNodeVersion(versionString = process.versions.node) {
|
|
|
39
39
|
* `HQ_SKIP_NODE_PREFLIGHT=1` to bypass (used by the test runner, which already
|
|
40
40
|
* runs on a supported Node).
|
|
41
41
|
*/
|
|
42
|
-
export function enforceNodeVersion() {
|
|
43
|
-
if (process.env.HQ_SKIP_NODE_PREFLIGHT)
|
|
42
|
+
export function enforceNodeVersion(options = {}) {
|
|
43
|
+
if (options.skip ?? process.env.HQ_SKIP_NODE_PREFLIGHT)
|
|
44
44
|
return;
|
|
45
|
-
const result = checkNodeVersion();
|
|
45
|
+
const result = checkNodeVersion(options.versionString ?? process.versions.node);
|
|
46
46
|
if (!result.ok && result.message) {
|
|
47
|
-
process.stderr.write(`${result.message}\n`);
|
|
48
|
-
process.exit(1);
|
|
47
|
+
(options.stderr ?? process.stderr).write(`${result.message}\n`);
|
|
48
|
+
(options.exit ?? process.exit)(1);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
enforceNodeVersion();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function readHqVersion(hqRoot: string): string | null;
|
|
2
|
+
export interface CatalogEntry {
|
|
3
|
+
source: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
conditional?: string;
|
|
6
|
+
auto_install?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Read recommended_packages from core.yaml (the curated content-pack catalog). */
|
|
9
|
+
export declare function readRecommendedPackages(hqRoot: string): CatalogEntry[];
|
|
10
|
+
//# sourceMappingURL=hq-core-yaml.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as yaml from "js-yaml";
|
|
4
|
+
/** Locate core.yaml: v14+ nests it under core/, older layouts at the root. */
|
|
5
|
+
function coreYamlPath(hqRoot) {
|
|
6
|
+
const nested = path.join(hqRoot, "core", "core.yaml");
|
|
7
|
+
if (fs.existsSync(nested))
|
|
8
|
+
return nested;
|
|
9
|
+
const flat = path.join(hqRoot, "core.yaml");
|
|
10
|
+
if (fs.existsSync(flat))
|
|
11
|
+
return flat;
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
export function readHqVersion(hqRoot) {
|
|
15
|
+
const filePath = coreYamlPath(hqRoot);
|
|
16
|
+
if (!filePath)
|
|
17
|
+
return null;
|
|
18
|
+
try {
|
|
19
|
+
const config = yaml.load(fs.readFileSync(filePath, "utf-8"));
|
|
20
|
+
return config?.hqVersion ?? null;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Read recommended_packages from core.yaml (the curated content-pack catalog). */
|
|
27
|
+
export function readRecommendedPackages(hqRoot) {
|
|
28
|
+
const filePath = coreYamlPath(hqRoot);
|
|
29
|
+
if (!filePath)
|
|
30
|
+
return [];
|
|
31
|
+
try {
|
|
32
|
+
const config = yaml.load(fs.readFileSync(filePath, "utf-8"));
|
|
33
|
+
const list = config?.recommended_packages;
|
|
34
|
+
return Array.isArray(list) ? list : [];
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=hq-core-yaml.js.map
|
|
@@ -39,6 +39,8 @@
|
|
|
39
39
|
import type { PackManifest, PackContributeKey } from '../types.js';
|
|
40
40
|
import { CONTRIBUTION_TABLE, type WireMode } from './contribution-table.js';
|
|
41
41
|
import { type UnregisterServerResult, type SafeWriteEnv, type AcquireLockOptions } from '../commands/mcp-registration.js';
|
|
42
|
+
export { readHqVersion, readRecommendedPackages } from './hq-core-yaml.js';
|
|
43
|
+
export type { CatalogEntry } from './hq-core-yaml.js';
|
|
42
44
|
export { CONTRIBUTION_TABLE };
|
|
43
45
|
export type { WireMode };
|
|
44
46
|
/**
|
|
@@ -163,13 +165,4 @@ export interface UnwirePackMcpOptions {
|
|
|
163
165
|
* @param contributes the pack's `contributes` block (its `mcp` list drives removal)
|
|
164
166
|
*/
|
|
165
167
|
export declare function unwirePackMcp(packName: string, contributes: Partial<Record<PackContributeKey, string[]>>, options?: UnwirePackMcpOptions): UnwireMcpResult;
|
|
166
|
-
export declare function readHqVersion(hqRoot: string): string | null;
|
|
167
|
-
export interface CatalogEntry {
|
|
168
|
-
source: string;
|
|
169
|
-
description?: string;
|
|
170
|
-
conditional?: string;
|
|
171
|
-
auto_install?: boolean;
|
|
172
|
-
}
|
|
173
|
-
/** Read `recommended_packages` from core.yaml (the curated content-pack catalog). */
|
|
174
|
-
export declare function readRecommendedPackages(hqRoot: string): CatalogEntry[];
|
|
175
168
|
//# sourceMappingURL=pack-contributions.d.ts.map
|
|
@@ -41,6 +41,7 @@ import * as path from 'path';
|
|
|
41
41
|
import * as yaml from 'js-yaml';
|
|
42
42
|
import { CONTRIBUTION_TABLE, payloadFor } from './contribution-table.js';
|
|
43
43
|
import { unregisterMcpServers, } from '../commands/mcp-registration.js';
|
|
44
|
+
export { readHqVersion, readRecommendedPackages } from './hq-core-yaml.js';
|
|
44
45
|
// NOTE on dependency direction: this module imports from ../commands/mcp-registration.js
|
|
45
46
|
// to un-register `wire:merge` (mcp) servers on uninstall. mcp-registration.ts imports
|
|
46
47
|
// ONLY fs/os/path/smol-toml (it does NOT import from pack-contributions.ts), so there
|
|
@@ -343,43 +344,4 @@ export function unwirePackMcp(packName, contributes, options) {
|
|
|
343
344
|
}
|
|
344
345
|
return { servers };
|
|
345
346
|
}
|
|
346
|
-
// ---------------------------------------------------------------------------
|
|
347
|
-
// Host introspection: hqVersion + recommended_packages catalog
|
|
348
|
-
// ---------------------------------------------------------------------------
|
|
349
|
-
/** Locate core.yaml -- v14+ nests it under `core/`, older layouts at the root. */
|
|
350
|
-
function coreYamlPath(hqRoot) {
|
|
351
|
-
const nested = path.join(hqRoot, 'core', 'core.yaml');
|
|
352
|
-
if (fs.existsSync(nested))
|
|
353
|
-
return nested;
|
|
354
|
-
const flat = path.join(hqRoot, 'core.yaml');
|
|
355
|
-
if (fs.existsSync(flat))
|
|
356
|
-
return flat;
|
|
357
|
-
return null;
|
|
358
|
-
}
|
|
359
|
-
export function readHqVersion(hqRoot) {
|
|
360
|
-
const p = coreYamlPath(hqRoot);
|
|
361
|
-
if (!p)
|
|
362
|
-
return null;
|
|
363
|
-
try {
|
|
364
|
-
const c = yaml.load(fs.readFileSync(p, 'utf-8'));
|
|
365
|
-
return c?.hqVersion ?? null;
|
|
366
|
-
}
|
|
367
|
-
catch {
|
|
368
|
-
return null;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
/** Read `recommended_packages` from core.yaml (the curated content-pack catalog). */
|
|
372
|
-
export function readRecommendedPackages(hqRoot) {
|
|
373
|
-
const p = coreYamlPath(hqRoot);
|
|
374
|
-
if (!p)
|
|
375
|
-
return [];
|
|
376
|
-
try {
|
|
377
|
-
const c = yaml.load(fs.readFileSync(p, 'utf-8'));
|
|
378
|
-
const list = c?.recommended_packages;
|
|
379
|
-
return Array.isArray(list) ? list : [];
|
|
380
|
-
}
|
|
381
|
-
catch {
|
|
382
|
-
return [];
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
347
|
//# sourceMappingURL=pack-contributions.js.map
|
|
@@ -6,6 +6,8 @@ function forbiddenFastPathModules(loadedModules, distDirectory, allowedCommandMo
|
|
|
6
6
|
const commands = `${path.join(dist, "commands")}${path.sep}`;
|
|
7
7
|
const flagRegistry = path.join(dist, "lib", "flag-registry.js");
|
|
8
8
|
const coreCommand = path.join(dist, "commands", "core.js");
|
|
9
|
+
const fullNetworkCompat = path.join(dist, "node-network-compat.js");
|
|
10
|
+
const sentryEntrypoint = path.join(dist, "sentry.js");
|
|
9
11
|
const allowedCommands = new Set(allowedCommandModules.map((module) => path.resolve(module)));
|
|
10
12
|
return [...new Set(loadedModules.map((loaded) => {
|
|
11
13
|
if (loaded.startsWith("file:")) {
|
|
@@ -22,7 +24,9 @@ function forbiddenFastPathModules(loadedModules, distDirectory, allowedCommandMo
|
|
|
22
24
|
path.resolve(loaded).startsWith(commands) ||
|
|
23
25
|
path.resolve(loaded) === flagRegistry ||
|
|
24
26
|
path.resolve(loaded) === coreCommand ||
|
|
25
|
-
path.resolve(loaded)
|
|
27
|
+
path.resolve(loaded) === fullNetworkCompat ||
|
|
28
|
+
path.resolve(loaded) === sentryEntrypoint ||
|
|
29
|
+
path.resolve(loaded).includes(`${path.sep}node_modules${path.sep}@sentry${path.sep}`))
|
|
26
30
|
.filter((loaded) => !allowedCommands.has(path.resolve(loaded)) ||
|
|
27
31
|
path.resolve(loaded) === main || path.resolve(loaded) === coreCommand)
|
|
28
32
|
.sort();
|
|
@@ -12,7 +12,7 @@ import semver from "semver";
|
|
|
12
12
|
import { CLI_VERSION } from "../cli-version.js";
|
|
13
13
|
import { wantsJson } from "../version-request.js";
|
|
14
14
|
import { findHqRoot } from "./manifest.js";
|
|
15
|
-
import { readHqVersion } from "./
|
|
15
|
+
import { readHqVersion } from "./hq-core-yaml.js";
|
|
16
16
|
export const HQ_CORE_LATEST_URL = "https://api.github.com/repos/indigoai-us/hq-core/releases/latest";
|
|
17
17
|
export const LATEST_FETCH_TIMEOUT_MS = 3_000;
|
|
18
18
|
export function normalizeHqCoreTag(tag) {
|