@indigoai-us/hq-cli 5.311.3 → 5.312.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 +16 -0
- package/dist/command-catalog.generated.d.ts +95 -0
- package/dist/command-catalog.generated.js +120 -0
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/cloud-provision.d.ts +3 -0
- package/dist/commands/cloud-provision.js +34 -1
- package/dist/commands/daemon.js +22 -0
- package/dist/commands/dm.js +22 -6
- package/dist/commands/files-browse.js +16 -4
- package/dist/commands/lanes-message.js +17 -2
- package/dist/commands/lanes-shared.d.ts +1 -0
- package/dist/commands/lanes.js +96 -20
- package/dist/commands/vault-retention-flag.d.ts +3 -0
- package/dist/commands/vault-retention-flag.js +9 -0
- package/dist/commands/vault-retention.d.ts +11 -0
- package/dist/commands/vault-retention.js +69 -0
- package/dist/lib/core-utils/sentry-report.js +21 -4
- package/dist/lib/daemon/catalog.d.ts +2 -0
- package/dist/lib/daemon/catalog.js +13 -1
- package/dist/lib/daemon/config.d.ts +2 -0
- package/dist/lib/daemon/config.js +4 -0
- package/dist/lib/daemon/run.d.ts +2 -0
- package/dist/lib/daemon/run.js +5 -0
- package/dist/lib/lanes/autopilot.d.ts +38 -1
- package/dist/lib/lanes/autopilot.js +353 -5
- package/dist/lib/lanes/flag-keys.d.ts +9 -1
- package/dist/lib/lanes/flag-keys.js +9 -1
- package/dist/lib/lanes/keep-alive.d.ts +2 -0
- package/dist/lib/lanes/keep-alive.js +2 -0
- package/dist/lib/lanes/reconcile.d.ts +5 -0
- package/dist/lib/lanes/reconcile.js +57 -4
- package/dist/lib/lanes/watcher-main.js +20 -1
- package/dist/lib/lanes/watcher.d.ts +9 -0
- package/dist/lib/lanes/watcher.js +47 -1
- package/package.json +1 -1
package/dist/commands/lanes.js
CHANGED
|
@@ -33,6 +33,7 @@ import { withCliFeatureUsed } from "../utils/cli-telemetry.js";
|
|
|
33
33
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
34
34
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
35
35
|
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
36
|
+
import { LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG } from "../lib/lanes/flag-keys.js";
|
|
36
37
|
import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
|
|
37
38
|
import { runLeadAutopilotPass } from "../lib/lanes/autopilot.js";
|
|
38
39
|
import { syncOutpostLeadRegistries } from "../lib/lanes/autopilot-registry.js";
|
|
@@ -1197,14 +1198,20 @@ async function readOutpostLaneRecordsUncached(ctx, outpostId, query, dispatchOpt
|
|
|
1197
1198
|
throw new LanesError("outpost_lane_list_unavailable", `Could not read lanes from Outpost ${outpostId}: ${message}`, { outpost_id: outpostId });
|
|
1198
1199
|
}
|
|
1199
1200
|
if (result.done !== true || result.status !== "Success" || result.exitCode !== 0) {
|
|
1200
|
-
throw new LanesError("outpost_lane_list_unavailable", `Outpost ${outpostId} did not return a successful lane list`, {
|
|
1201
|
+
throw new LanesError("outpost_lane_list_unavailable", `Outpost ${outpostId} did not return a successful lane list`, {
|
|
1202
|
+
outpost_id: outpostId,
|
|
1203
|
+
command_id: result.commandId,
|
|
1204
|
+
status: result.status,
|
|
1205
|
+
done: result.done ?? false,
|
|
1206
|
+
exit_code: result.exitCode,
|
|
1207
|
+
});
|
|
1201
1208
|
}
|
|
1202
1209
|
let body;
|
|
1203
1210
|
try {
|
|
1204
1211
|
body = JSON.parse(result.stdout);
|
|
1205
1212
|
}
|
|
1206
1213
|
catch (err) {
|
|
1207
|
-
throw new LanesError("outpost_lane_list_invalid", `Outpost ${outpostId} returned invalid lane-list JSON: ${dispatchErrorMessage(err)}`, { outpost_id: outpostId });
|
|
1214
|
+
throw new LanesError("outpost_lane_list_invalid", `Outpost ${outpostId} returned invalid lane-list JSON: ${dispatchErrorMessage(err)}`, { outpost_id: outpostId, command_id: result.commandId });
|
|
1208
1215
|
}
|
|
1209
1216
|
if (body === null || typeof body !== "object" || Array.isArray(body) ||
|
|
1210
1217
|
body.ok !== true ||
|
|
@@ -1212,6 +1219,7 @@ async function readOutpostLaneRecordsUncached(ctx, outpostId, query, dispatchOpt
|
|
|
1212
1219
|
!Array.isArray(body.lanes)) {
|
|
1213
1220
|
throw new LanesError("outpost_lane_list_invalid", `Outpost ${outpostId} returned a malformed lane list`, {
|
|
1214
1221
|
outpost_id: outpostId,
|
|
1222
|
+
command_id: result.commandId,
|
|
1215
1223
|
});
|
|
1216
1224
|
}
|
|
1217
1225
|
const listed = body.lanes;
|
|
@@ -1499,7 +1507,12 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
|
|
|
1499
1507
|
}
|
|
1500
1508
|
const separator = laneId.lastIndexOf("_");
|
|
1501
1509
|
const hostSuffix = separator < 0 ? "" : laneId.slice(separator + 1);
|
|
1502
|
-
|
|
1510
|
+
const ownerCompany = !hostSuffix && requestedUlid !== undefined && options.allowHostlessOwnerLookup
|
|
1511
|
+
? await options.allowHostlessOwnerLookup()
|
|
1512
|
+
: undefined;
|
|
1513
|
+
const hostlessUlidLookup = ownerCompany !== undefined;
|
|
1514
|
+
if ((!hostSuffix && !hostlessUlidLookup) ||
|
|
1515
|
+
(hostSuffix && sanitizeHostComponent(hostSuffix) !== hostSuffix)) {
|
|
1503
1516
|
throw commandLaneNotFound(laneId, options.outpostHint === true);
|
|
1504
1517
|
}
|
|
1505
1518
|
let rawOutpostRoutes;
|
|
@@ -1542,7 +1555,9 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
|
|
|
1542
1555
|
if (requestedOutpostId !== undefined && !outpostIds.includes(requestedOutpostId)) {
|
|
1543
1556
|
throw new LanesError("outpost_not_registered", `Outpost ${requestedOutpostId} is not registered`, { lane_id: laneId, outpost_id: requestedOutpostId });
|
|
1544
1557
|
}
|
|
1545
|
-
const hostRoutes =
|
|
1558
|
+
const hostRoutes = hostlessUlidLookup
|
|
1559
|
+
? []
|
|
1560
|
+
: outpostRoutes.filter((route) => outpostRouteMatchesHost(route, hostSuffix));
|
|
1546
1561
|
const identity = ctx.readOutpostIdentity();
|
|
1547
1562
|
const localHostRoutes = outpostRoutes.filter((route) => outpostRouteMatchesHost(route, ctx.hostname()));
|
|
1548
1563
|
if (localHostRoutes.length > 1 || (identity.kind === "id" && localHostRoutes.some((route) => route.outpostId !== identity.id))) {
|
|
@@ -1553,20 +1568,22 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
|
|
|
1553
1568
|
const hostOutpostIdSet = new Set(hostOutpostIds);
|
|
1554
1569
|
const matches = [];
|
|
1555
1570
|
const failedOutpostIds = [];
|
|
1571
|
+
const failedOutpostCommands = [];
|
|
1556
1572
|
const probedOutpostIds = [];
|
|
1557
1573
|
const lookupOutpostIds = requestedOutpostId !== undefined
|
|
1558
1574
|
? [requestedOutpostId]
|
|
1559
|
-
:
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1575
|
+
: hostlessUlidLookup
|
|
1576
|
+
? outpostIds.filter((id) => id !== localOutpostId)
|
|
1577
|
+
: [
|
|
1578
|
+
...hostOutpostIds.filter((id) => id !== localOutpostId),
|
|
1579
|
+
...outpostIds.filter((id) => id !== localOutpostId && !hostOutpostIdSet.has(id)),
|
|
1580
|
+
];
|
|
1563
1581
|
for (const outpostId of lookupOutpostIds) {
|
|
1564
1582
|
let ownerLanes;
|
|
1565
1583
|
try {
|
|
1566
|
-
ownerLanes = await readOutpostLaneRecords(ctx, outpostId,
|
|
1567
|
-
|
|
1568
|
-
snapshot: true
|
|
1569
|
-
});
|
|
1584
|
+
ownerLanes = await readOutpostLaneRecords(ctx, outpostId, hostlessUlidLookup
|
|
1585
|
+
? { snapshot: true }
|
|
1586
|
+
: { hierarchy: [laneId], snapshot: true });
|
|
1570
1587
|
probedOutpostIds.push(outpostId);
|
|
1571
1588
|
}
|
|
1572
1589
|
catch (err) {
|
|
@@ -1575,16 +1592,38 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
|
|
|
1575
1592
|
if (isLanesError(err) && err.error === "outpost_wait_invalid")
|
|
1576
1593
|
throw err;
|
|
1577
1594
|
failedOutpostIds.push(outpostId);
|
|
1595
|
+
failedOutpostCommands.push({
|
|
1596
|
+
outpost_id: outpostId,
|
|
1597
|
+
...(isLanesError(err) && typeof err.details.command_id === "string"
|
|
1598
|
+
? { command_id: err.details.command_id }
|
|
1599
|
+
: {}),
|
|
1600
|
+
});
|
|
1578
1601
|
continue;
|
|
1579
1602
|
}
|
|
1580
1603
|
for (const lane of ownerLanes) {
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1604
|
+
const recordedSuffix = lane.lane_id.slice(lane.lane_id.lastIndexOf("_") + 1);
|
|
1605
|
+
const laneCompany = hostlessUlidLookup
|
|
1606
|
+
? resolveLaneCompany(ctx.hqRoot(), lane).company
|
|
1607
|
+
: undefined;
|
|
1608
|
+
const companyMatches = !hostlessUlidLookup || (ownerCompany !== undefined &&
|
|
1609
|
+
laneCompany?.uid === ownerCompany.uid &&
|
|
1610
|
+
laneCompany.slug === ownerCompany.slug);
|
|
1611
|
+
if (lane.host.kind === "local" && ((!hostlessUlidLookup && lane.lane_id === laneId &&
|
|
1612
|
+
sanitizeHostComponent(lane.host.id) === hostSuffix) ||
|
|
1613
|
+
(hostlessUlidLookup && laneUlidPart(lane.lane_id) === requestedUlid &&
|
|
1614
|
+
sanitizeHostComponent(lane.host.id) === recordedSuffix &&
|
|
1615
|
+
companyMatches))) {
|
|
1584
1616
|
matches.push({ outpostId, lane });
|
|
1585
1617
|
}
|
|
1586
1618
|
}
|
|
1587
1619
|
}
|
|
1620
|
+
if (hostlessUlidLookup && failedOutpostIds.length > 0) {
|
|
1621
|
+
throw new LanesError("lane_owner_lookup_unavailable", `Could not verify whether lane ${laneId} is registered on every Outpost`, {
|
|
1622
|
+
lane_id: laneId,
|
|
1623
|
+
failed_outpost_ids: failedOutpostIds,
|
|
1624
|
+
remote_commands: failedOutpostCommands,
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1588
1627
|
if (matches.length === 0) {
|
|
1589
1628
|
throw commandLaneNotFound(laneId, options.outpostHint === true, {
|
|
1590
1629
|
...(failedOutpostIds.length > 0 ? { failed_outpost_ids: failedOutpostIds } : {}),
|
|
@@ -1604,9 +1643,9 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
|
|
|
1604
1643
|
}
|
|
1605
1644
|
function laneUlidPart(laneId) {
|
|
1606
1645
|
const separator = laneId.indexOf("_");
|
|
1607
|
-
if (separator !== 26)
|
|
1646
|
+
if (separator !== -1 && separator !== 26)
|
|
1608
1647
|
return undefined;
|
|
1609
|
-
const ulid = laneId.slice(0, separator);
|
|
1648
|
+
const ulid = separator === -1 ? laneId : laneId.slice(0, separator);
|
|
1610
1649
|
return /^[0-9A-HJKMNP-TV-Z]{26}$/i.test(ulid) ? ulid.toUpperCase() : undefined;
|
|
1611
1650
|
}
|
|
1612
1651
|
function commandLaneNotFound(laneId, outpostHint, details = {}) {
|
|
@@ -7660,7 +7699,7 @@ function writeAnswerDeliveryFailed(hqRoot, laneId, questionId, err, at) {
|
|
|
7660
7699
|
async function runReconcile(ctx, opts, recoverQueuedMessages) {
|
|
7661
7700
|
const fromEnv = ctx.env.HQ_ROOT?.trim();
|
|
7662
7701
|
const hqRoot = fromEnv && ctx.existsSync(fromEnv) ? fromEnv : ctx.hqRoot();
|
|
7663
|
-
const mirrorRefresh = opts.dryRun
|
|
7702
|
+
const mirrorRefresh = opts.dryRun || opts.terminalKeepAliveCandidate
|
|
7664
7703
|
? {
|
|
7665
7704
|
updated: new Map(),
|
|
7666
7705
|
unverifiedRemote: new Map(),
|
|
@@ -7724,6 +7763,9 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
|
|
|
7724
7763
|
recoverQueuedMessages,
|
|
7725
7764
|
mesh: ctx.mesh,
|
|
7726
7765
|
env: ctx.env,
|
|
7766
|
+
...(opts.terminalKeepAliveCandidate !== undefined
|
|
7767
|
+
? { terminalKeepAliveCandidate: opts.terminalKeepAliveCandidate }
|
|
7768
|
+
: {}),
|
|
7727
7769
|
prepareKeepAliveClaudeResume: async (lane) => {
|
|
7728
7770
|
const recoveredLane = laneWithRecoveredProviderSession(hqRoot, lane);
|
|
7729
7771
|
const pendingModel = recoveredLane.pending_settings?.model;
|
|
@@ -8041,7 +8083,16 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8041
8083
|
persistClaudeAccountSelection,
|
|
8042
8084
|
printJson,
|
|
8043
8085
|
recordResumeSetup,
|
|
8044
|
-
resolveCommandLane: (commandCtx, laneId) => resolveCommandLaneTarget(commandCtx, laneId
|
|
8086
|
+
resolveCommandLane: (commandCtx, laneId) => resolveCommandLaneTarget(commandCtx, laneId, {
|
|
8087
|
+
allowHostlessOwnerLookup: async () => {
|
|
8088
|
+
const company = await commandCtx.resolveCompany();
|
|
8089
|
+
const enabled = await readLanesRegistryFlag(LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, {
|
|
8090
|
+
company,
|
|
8091
|
+
env: commandCtx.env,
|
|
8092
|
+
});
|
|
8093
|
+
return enabled ? company : undefined;
|
|
8094
|
+
},
|
|
8095
|
+
}),
|
|
8045
8096
|
resolveActiveQuestionCompany,
|
|
8046
8097
|
hasPendingQuestionForLane: questionHandlers.hasPendingQuestionForLane,
|
|
8047
8098
|
cancelFallbackQuestionsForLane: questionHandlers.cancelFallbackQuestionsForLane,
|
|
@@ -8406,7 +8457,8 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8406
8457
|
.command("message")
|
|
8407
8458
|
.description("Send a follow-up message to a lane")
|
|
8408
8459
|
.argument("<lane-id>", "Lane id")
|
|
8409
|
-
.
|
|
8460
|
+
.option("--text <msg>", "Message text")
|
|
8461
|
+
.option("--text-file <path>", "Read message text from a file")
|
|
8410
8462
|
.option("--project <slug>", "Re-point project attribution; a changed story is named in the resumed prompt")
|
|
8411
8463
|
.option("--story <id>", "Re-point story attribution; a changed story is named in the resumed prompt")
|
|
8412
8464
|
.option("--account <id>", "Move the lane's session to this account of its provider before the message resumes it")
|
|
@@ -8745,6 +8797,30 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8745
8797
|
await messageHandlers.recoverQueuedMessages(ctx, lane.lane_id);
|
|
8746
8798
|
});
|
|
8747
8799
|
}));
|
|
8800
|
+
lanes
|
|
8801
|
+
.command("_keep-alive-terminal-trigger <laneId> <version> <dueAt>", { hidden: true })
|
|
8802
|
+
.action(wrap(async (laneId, versionText, dueAt) => {
|
|
8803
|
+
const version = Number.parseInt(versionText, 10);
|
|
8804
|
+
if (!Number.isSafeInteger(version) || version < 1 || String(version) !== versionText) {
|
|
8805
|
+
throw new LanesError("invalid_lane_version", "Terminal keep-alive trigger requires a positive lane version", {
|
|
8806
|
+
lane_id: laneId,
|
|
8807
|
+
version: versionText,
|
|
8808
|
+
});
|
|
8809
|
+
}
|
|
8810
|
+
const dueAtMs = Date.parse(dueAt);
|
|
8811
|
+
if (!Number.isFinite(dueAtMs)) {
|
|
8812
|
+
throw new LanesError("invalid_terminal_trigger_time", "Terminal keep-alive trigger requires a valid due time", {
|
|
8813
|
+
lane_id: laneId,
|
|
8814
|
+
due_at: dueAt,
|
|
8815
|
+
});
|
|
8816
|
+
}
|
|
8817
|
+
const delayMs = dueAtMs - Date.now();
|
|
8818
|
+
if (delayMs > 0)
|
|
8819
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
8820
|
+
await runReconcile(ctx, { terminalKeepAliveCandidate: { laneId, version } }, async (lane) => {
|
|
8821
|
+
await messageHandlers.recoverQueuedMessages(ctx, lane.lane_id);
|
|
8822
|
+
});
|
|
8823
|
+
}));
|
|
8748
8824
|
reconcile
|
|
8749
8825
|
.command("install")
|
|
8750
8826
|
.description("Write systemd user unit and timer files (idempotent)")
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { resolveCompanyFlagGate, resolveProcessFlagGate } from "../lib/flag-registry.js";
|
|
2
|
+
export const VAULT_RETENTION_SETTINGS_FLAG = "cli.vault-retention-settings";
|
|
3
|
+
export function vaultRetentionSettingsEnabled(companyUid, idToken) {
|
|
4
|
+
if (companyUid && idToken) {
|
|
5
|
+
return resolveCompanyFlagGate(companyUid, idToken, VAULT_RETENTION_SETTINGS_FLAG, { fallback: false }, () => false);
|
|
6
|
+
}
|
|
7
|
+
return resolveProcessFlagGate(VAULT_RETENTION_SETTINGS_FLAG, { fallback: false }, () => false);
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=vault-retention-flag.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
3
|
+
type Dependencies = {
|
|
4
|
+
enabled: (companyUid?: string, token?: string) => boolean | Promise<boolean>;
|
|
5
|
+
token: () => Promise<string>;
|
|
6
|
+
companyUid: (token: string, slug: string | undefined) => Promise<string>;
|
|
7
|
+
request: typeof vaultApiFetch;
|
|
8
|
+
};
|
|
9
|
+
export declare function registerVaultRetentionCommand(program: Command, deps?: Dependencies): void;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=vault-retention.d.ts.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
2
|
+
import { markExpectedUserError } from "../utils/expected-cli-error.js";
|
|
3
|
+
import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
|
|
4
|
+
import { vaultRetentionSettingsEnabled } from "./vault-retention-flag.js";
|
|
5
|
+
const defaultDependencies = {
|
|
6
|
+
enabled: vaultRetentionSettingsEnabled,
|
|
7
|
+
token: ensureCognitoToken,
|
|
8
|
+
companyUid: getCompanyUid,
|
|
9
|
+
request: vaultApiFetch,
|
|
10
|
+
};
|
|
11
|
+
export function registerVaultRetentionCommand(program, deps = defaultDependencies) {
|
|
12
|
+
const vault = program.command("vault").description("Manage HQ vault settings");
|
|
13
|
+
const retention = vault
|
|
14
|
+
.command("retention")
|
|
15
|
+
.description("Get or set the default retention for future vault object versions")
|
|
16
|
+
.option("--company <slug>", "Company vault slug (omit for your personal vault)");
|
|
17
|
+
const companyOf = (command) => command.opts().company ??
|
|
18
|
+
retention.opts().company;
|
|
19
|
+
retention
|
|
20
|
+
.command("get")
|
|
21
|
+
.description("Show the vault's default retention in days")
|
|
22
|
+
.action(async function () {
|
|
23
|
+
await execute(this, "GET");
|
|
24
|
+
});
|
|
25
|
+
retention
|
|
26
|
+
.command("set")
|
|
27
|
+
.description("Set the vault's default retention in days (1-30)")
|
|
28
|
+
.argument("<days>")
|
|
29
|
+
.action(async function (rawDays) {
|
|
30
|
+
const days = Number(rawDays);
|
|
31
|
+
if (!Number.isInteger(days) || days < 1 || days > 30) {
|
|
32
|
+
throw new Error("days must be an integer from 1 through 30");
|
|
33
|
+
}
|
|
34
|
+
await execute(this, "PUT", days);
|
|
35
|
+
});
|
|
36
|
+
async function execute(command, method, days) {
|
|
37
|
+
const slug = companyOf(command);
|
|
38
|
+
let token;
|
|
39
|
+
let companyUid;
|
|
40
|
+
if (slug) {
|
|
41
|
+
token = await deps.token();
|
|
42
|
+
companyUid = await deps.companyUid(token, slug);
|
|
43
|
+
if (!(await deps.enabled(companyUid, token))) {
|
|
44
|
+
throw markExpectedUserError(new Error("Vault retention settings are not enabled for this company."));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
if (!(await deps.enabled())) {
|
|
49
|
+
throw markExpectedUserError(new Error("Vault retention settings are not enabled for this company."));
|
|
50
|
+
}
|
|
51
|
+
token = await deps.token();
|
|
52
|
+
}
|
|
53
|
+
const query = companyUid ? { companyUid } : undefined;
|
|
54
|
+
const response = await deps.request({
|
|
55
|
+
token,
|
|
56
|
+
path: "/vault/retention",
|
|
57
|
+
method,
|
|
58
|
+
...(query ? { query } : {}),
|
|
59
|
+
...(days === undefined ? {} : { body: { days } }),
|
|
60
|
+
});
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
const body = (await response.json().catch(() => ({})));
|
|
63
|
+
throw new Error(body.error ?? body.message ?? response.statusText);
|
|
64
|
+
}
|
|
65
|
+
const body = (await response.json());
|
|
66
|
+
process.stdout.write(`${body.days} days\n`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=vault-retention.js.map
|
|
@@ -20,6 +20,7 @@ import { mkdirSync, readdirSync, readFileSync, renameSync, rmdirSync, statSync,
|
|
|
20
20
|
import { tmpdir } from "node:os";
|
|
21
21
|
import { dirname, join } from "node:path";
|
|
22
22
|
import * as Sentry from "@sentry/node";
|
|
23
|
+
import { gte as semverGte, valid as validSemver } from "semver";
|
|
23
24
|
import { CLI_VERSION } from "../../cli-version.js";
|
|
24
25
|
import { beforeSend } from "../../sentry-before-send.js";
|
|
25
26
|
import { resolveLiveRoot } from "../../utils/hq-roots.js";
|
|
@@ -47,6 +48,10 @@ const HOOK_TIMEOUT_EXCEEDED_METRIC = "hq.hook_timeout_exceeded";
|
|
|
47
48
|
const MAX_HOOK_SEQUENCE_ENTRIES = 20;
|
|
48
49
|
const MAX_HOOK_TIMEOUT_DEBUG_PHASES = 24;
|
|
49
50
|
const MAX_HOOK_TIMEOUT_DEBUG_MS = 86_400_000;
|
|
51
|
+
// This is the hqVersion embedded in the first hq-core tag whose master-hook
|
|
52
|
+
// replaces BASH_ENV with /dev/null before reading it (tag v15.0.157-beta.2).
|
|
53
|
+
// Earlier cores only report whether BASH_ENV is non-empty, which is ambiguous.
|
|
54
|
+
const BASH_ENV_NEUTRALIZED_CORE_VERSION = "15.0.157-beta.2";
|
|
50
55
|
const HOOK_TIMEOUT_RATE_LIMIT_WINDOW_MS = 10 * 60 * 1_000;
|
|
51
56
|
const RATE_LIMIT_LOCK_WAIT_MS = 500;
|
|
52
57
|
const RATE_LIMIT_LOCK_POLL_MS = 5;
|
|
@@ -578,9 +583,10 @@ function hookTimeoutDebugContextTags(context) {
|
|
|
578
583
|
function safeSlowChild(metadata) {
|
|
579
584
|
const value = metadata.slow_child;
|
|
580
585
|
const slowChildMs = metadata.slow_child_ms;
|
|
586
|
+
const bashEnv = bashEnvTag(metadata);
|
|
581
587
|
const hasMeasuredChild = typeof slowChildMs === "number" && Number.isFinite(slowChildMs) && slowChildMs >= 0;
|
|
582
588
|
if (metadata.hook_event === "SessionStart" &&
|
|
583
|
-
(
|
|
589
|
+
(bashEnv === "profile" || bashEnv === "set") &&
|
|
584
590
|
!hasMeasuredChild &&
|
|
585
591
|
(typeof value !== "string" || value.length === 0 || value === "other")) {
|
|
586
592
|
return "entry-bash-startup";
|
|
@@ -605,8 +611,15 @@ function isMachineWideHookSlowness(metadata) {
|
|
|
605
611
|
}
|
|
606
612
|
function bashEnvTag(metadata) {
|
|
607
613
|
const value = metadata.bash_env_set;
|
|
608
|
-
if (value ===
|
|
609
|
-
return
|
|
614
|
+
if (value === "profile" || value === "devnull")
|
|
615
|
+
return value;
|
|
616
|
+
if (value === true || value === "set") {
|
|
617
|
+
const coreVersion = metadata.hq_version;
|
|
618
|
+
const parsedCoreVersion = typeof coreVersion === "string" ? validSemver(coreVersion) : null;
|
|
619
|
+
if (!parsedCoreVersion)
|
|
620
|
+
return "unknown";
|
|
621
|
+
return semverGte(parsedCoreVersion, BASH_ENV_NEUTRALIZED_CORE_VERSION) ? "devnull" : "set";
|
|
622
|
+
}
|
|
610
623
|
if (value === false || value === "unset")
|
|
611
624
|
return "unset";
|
|
612
625
|
return "unknown";
|
|
@@ -882,9 +895,13 @@ function timeoutRateLimitLease(event, kind, options, dependencies) {
|
|
|
882
895
|
}
|
|
883
896
|
}
|
|
884
897
|
function metricAttributes(metadata) {
|
|
885
|
-
|
|
898
|
+
const attributes = Object.fromEntries(Object.entries(metadata)
|
|
886
899
|
.filter(([, value]) => isScalarMetadataValue(value))
|
|
887
900
|
.map(([key, value]) => [key, typeof value === "string" ? safeTagValue(value) : value]));
|
|
901
|
+
if (Object.hasOwn(metadata, "bash_env_set")) {
|
|
902
|
+
attributes.bash_env_set = bashEnvTag(metadata);
|
|
903
|
+
}
|
|
904
|
+
return attributes;
|
|
888
905
|
}
|
|
889
906
|
function addHookSequenceBreadcrumbs(event, dependencies) {
|
|
890
907
|
const sequence = event.metadata.hook_sequence;
|
|
@@ -79,6 +79,8 @@ export interface PlanContext {
|
|
|
79
79
|
sessionHostRunnerPath: string | null;
|
|
80
80
|
/** Environment retained from legacy outpost sync systemd drop-ins. */
|
|
81
81
|
legacySyncEnv: Record<string, string>;
|
|
82
|
+
/** Keep the sync runner's event-driven instant-sync watcher enabled. */
|
|
83
|
+
instantSyncEnabled?: boolean;
|
|
82
84
|
/** Keep the local sync runner stopped until the operator resumes it. */
|
|
83
85
|
syncPaused: boolean;
|
|
84
86
|
/** Persisted/effective owner selected for this machine. */
|
|
@@ -128,7 +128,19 @@ function planService(service, ctx) {
|
|
|
128
128
|
command: {
|
|
129
129
|
type: "node",
|
|
130
130
|
script: ctx.syncRunnerPath,
|
|
131
|
-
args: [
|
|
131
|
+
args: [
|
|
132
|
+
"--companies",
|
|
133
|
+
"--direction",
|
|
134
|
+
"both",
|
|
135
|
+
"--on-conflict",
|
|
136
|
+
"keep",
|
|
137
|
+
"--hq-root",
|
|
138
|
+
ctx.hqRoot ?? "",
|
|
139
|
+
"--watch",
|
|
140
|
+
...(ctx.instantSyncEnabled === false ? [] : ["--event-push"]),
|
|
141
|
+
"--owner",
|
|
142
|
+
SYNC_RUNNER_OWNER,
|
|
143
|
+
],
|
|
132
144
|
},
|
|
133
145
|
env: { ...ctx.legacySyncEnv },
|
|
134
146
|
heartbeatTimeoutMs: ctx.syncHeartbeatTimeoutMs,
|
|
@@ -14,6 +14,8 @@ export interface DaemonConfig {
|
|
|
14
14
|
hqCloudVersion?: string;
|
|
15
15
|
/** Persisted machine-local sync pause. */
|
|
16
16
|
syncPaused?: boolean;
|
|
17
|
+
/** Desktop Instant Sync preference, applied when the sync unit is planned. */
|
|
18
|
+
instantSyncEnabled?: boolean;
|
|
17
19
|
/** Explicit desktop handoff; absent values preserve the legacy per-machine owner. */
|
|
18
20
|
syncOwner?: SyncOwner;
|
|
19
21
|
/** Services turned on here beyond the machine type's defaults. */
|
|
@@ -45,6 +45,8 @@ export function readDaemonConfig(file) {
|
|
|
45
45
|
enable: stringList(obj.enable),
|
|
46
46
|
disable: stringList(obj.disable),
|
|
47
47
|
};
|
|
48
|
+
if (typeof obj.instantSyncEnabled === "boolean")
|
|
49
|
+
config.instantSyncEnabled = obj.instantSyncEnabled;
|
|
48
50
|
if (isMachineType(obj.machineType))
|
|
49
51
|
config.machineType = obj.machineType;
|
|
50
52
|
if (obj.syncOwner === "daemon" || obj.syncOwner === "desktop-app")
|
|
@@ -67,6 +69,8 @@ export function writeDaemonConfig(file, config) {
|
|
|
67
69
|
if (config.hqCloudVersion)
|
|
68
70
|
ordered.hqCloudVersion = config.hqCloudVersion;
|
|
69
71
|
ordered.syncPaused = config.syncPaused === true;
|
|
72
|
+
if (typeof config.instantSyncEnabled === "boolean")
|
|
73
|
+
ordered.instantSyncEnabled = config.instantSyncEnabled;
|
|
70
74
|
if (config.syncOwner)
|
|
71
75
|
ordered.syncOwner = config.syncOwner;
|
|
72
76
|
ordered.enable = config.enable;
|
package/dist/lib/daemon/run.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface RunDaemonOptions {
|
|
|
24
24
|
hqBinary: string;
|
|
25
25
|
}
|
|
26
26
|
export declare function resolveSyncRunnerPath(): string | null;
|
|
27
|
+
/** The hosted desktop app uses this env value when planning daemon services. */
|
|
28
|
+
export declare function daemonInstantSyncEnabled(env: NodeJS.ProcessEnv, configured?: boolean): boolean;
|
|
27
29
|
export declare function listLocalBots(root?: string): string[];
|
|
28
30
|
export declare function windowsTaskExists(taskName: string): boolean;
|
|
29
31
|
export declare function probeLegacyUnits(platform: NodeJS.Platform, home: string): LegacyUnit[];
|
package/dist/lib/daemon/run.js
CHANGED
|
@@ -53,6 +53,10 @@ export function resolveSyncRunnerPath() {
|
|
|
53
53
|
return null;
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
+
/** The hosted desktop app uses this env value when planning daemon services. */
|
|
57
|
+
export function daemonInstantSyncEnabled(env, configured) {
|
|
58
|
+
return configured ?? env.HQ_DAEMON_INSTANT_SYNC !== "0";
|
|
59
|
+
}
|
|
56
60
|
export function listLocalBots(root = botsRoot()) {
|
|
57
61
|
if (!fs.existsSync(root))
|
|
58
62
|
return [];
|
|
@@ -447,6 +451,7 @@ export async function runDaemon(opts) {
|
|
|
447
451
|
heartbeatRunnerPath: runners.heartbeat,
|
|
448
452
|
sessionHostRunnerPath: runners.sessionHost,
|
|
449
453
|
legacySyncEnv: readLegacySyncEnv(platform),
|
|
454
|
+
instantSyncEnabled: daemonInstantSyncEnabled(env, config.instantSyncEnabled),
|
|
450
455
|
signedIn: isSignedIn,
|
|
451
456
|
kitConfigured: fs.existsSync(agentKitPaths(home, env).kitConfigPath),
|
|
452
457
|
syncPaused: config.syncPaused === true,
|
|
@@ -2,9 +2,13 @@ import { readLanesRegistryFlag } from "./registry-flag.js";
|
|
|
2
2
|
import type { LaneRecord } from "./types.js";
|
|
3
3
|
export declare const LEAD_AUTOPILOT_FLAG = "lanes.lead-autopilot";
|
|
4
4
|
export declare const LEAD_AUTOPILOT_DEFAULT_ENABLED = false;
|
|
5
|
+
export declare const LEAD_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG = "lanes.autopilot-missed-deadline-escalation";
|
|
6
|
+
export declare const LEAD_AUTOPILOT_MISSED_DEADLINE_ESCALATION_DEFAULT_ENABLED = false;
|
|
7
|
+
export declare const LEAD_AUTOPILOT_MISSED_DEADLINE_ESCALATION_DELAY_MS: number;
|
|
5
8
|
export declare const LEAD_AUTOPILOT_REVIEW_DELAY_MS: number;
|
|
6
9
|
export declare const LEAD_AUTOPILOT_FILL_DELAY_MS: number;
|
|
7
10
|
export declare const LEAD_AUTOPILOT_PROMPT_LOOP = "personal/knowledge/team-lead-operating-loop.md";
|
|
11
|
+
export declare const LEAD_AUTOPILOT_KPI_STALE_AFTER_MINUTES = 55;
|
|
8
12
|
export interface RegisteredLead {
|
|
9
13
|
team: string;
|
|
10
14
|
lane: string;
|
|
@@ -12,7 +16,7 @@ export interface RegisteredLead {
|
|
|
12
16
|
status: string;
|
|
13
17
|
prd: string;
|
|
14
18
|
}
|
|
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";
|
|
19
|
+
export type AutopilotCondition = "review_finished_worker" | "fill_grant" | "plan_free_slots" | "flat_kpi" | "stale_kpi" | "blocker_record_needs_update" | "project_ready_to_close" | "stale_worker_ledger" | "story_without_live_lane" | "missed_deadline";
|
|
16
20
|
export interface AutopilotWake {
|
|
17
21
|
condition: AutopilotCondition;
|
|
18
22
|
lead_lane: string;
|
|
@@ -20,6 +24,23 @@ export interface AutopilotWake {
|
|
|
20
24
|
worker_lane?: string;
|
|
21
25
|
prompt: string;
|
|
22
26
|
fingerprint: string;
|
|
27
|
+
kpi_id?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface AutopilotKpiStatus {
|
|
30
|
+
lead_lane: string;
|
|
31
|
+
team: string;
|
|
32
|
+
kpi: string;
|
|
33
|
+
age_minutes: number | null;
|
|
34
|
+
stale: boolean;
|
|
35
|
+
stale_after_minutes: number;
|
|
36
|
+
status?: string;
|
|
37
|
+
error?: "invalid_kpi";
|
|
38
|
+
}
|
|
39
|
+
interface AutopilotDeadlineError {
|
|
40
|
+
lead_lane: string;
|
|
41
|
+
item_ref: string;
|
|
42
|
+
field: string;
|
|
43
|
+
reason: string;
|
|
23
44
|
}
|
|
24
45
|
export interface AutopilotPassResult {
|
|
25
46
|
ok: true;
|
|
@@ -38,11 +59,23 @@ export interface AutopilotPassResult {
|
|
|
38
59
|
story_id: string;
|
|
39
60
|
at: string;
|
|
40
61
|
fingerprint: string;
|
|
62
|
+
owner?: string;
|
|
63
|
+
due?: string;
|
|
64
|
+
miss_count?: number;
|
|
65
|
+
}>;
|
|
66
|
+
missed_deadlines?: Array<{
|
|
67
|
+
lead_lane: string;
|
|
68
|
+
item: string;
|
|
69
|
+
owner: string;
|
|
70
|
+
due: string;
|
|
71
|
+
miss_count: number;
|
|
41
72
|
}>;
|
|
73
|
+
deadline_errors?: AutopilotDeadlineError[];
|
|
42
74
|
skipped: Array<{
|
|
43
75
|
lead_lane: string;
|
|
44
76
|
reason: string;
|
|
45
77
|
}>;
|
|
78
|
+
kpi_status?: AutopilotKpiStatus[];
|
|
46
79
|
/** Set on an Outpost that has not received a lead registry from the primary yet. */
|
|
47
80
|
registry?: "missing";
|
|
48
81
|
}
|
|
@@ -54,6 +87,7 @@ interface ProjectStory {
|
|
|
54
87
|
laneIds?: string[];
|
|
55
88
|
updatedAt?: string;
|
|
56
89
|
externalBlocker?: string;
|
|
90
|
+
deadlineFields?: Record<string, unknown>;
|
|
57
91
|
}
|
|
58
92
|
export declare function planLeadAutopilotWakes(input: {
|
|
59
93
|
lead: LaneRecord;
|
|
@@ -62,6 +96,7 @@ export declare function planLeadAutopilotWakes(input: {
|
|
|
62
96
|
now: Date;
|
|
63
97
|
managerStatus?: Record<string, unknown>;
|
|
64
98
|
keepAliveResumed?: boolean;
|
|
99
|
+
kpiStalenessEnabled?: boolean;
|
|
65
100
|
}): AutopilotWake[];
|
|
66
101
|
export declare function runLeadAutopilotPass(input: {
|
|
67
102
|
hqRoot: string;
|
|
@@ -71,7 +106,9 @@ export declare function runLeadAutopilotPass(input: {
|
|
|
71
106
|
now: Date;
|
|
72
107
|
dryRun?: boolean;
|
|
73
108
|
readFlag?: typeof readLanesRegistryFlag;
|
|
109
|
+
readDeadlineFlag?: typeof readLanesRegistryFlag;
|
|
74
110
|
keepAliveResumedLeads?: ReadonlySet<string>;
|
|
111
|
+
missedDeadlineEscalationDelayMs?: number;
|
|
75
112
|
startWorker?: (input: {
|
|
76
113
|
lead: LaneRecord;
|
|
77
114
|
story: ProjectStory;
|