@indigoai-us/hq-cli 5.313.0 → 5.315.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 +11 -0
- package/assets/scaffold/core/scripts/migrate-policy-triggers.sh +12 -7
- package/dist/command-catalog.generated.d.ts +18 -0
- package/dist/command-catalog.generated.js +23 -0
- package/dist/commands/core-migrate-policy-triggers-native.js +4 -8
- package/dist/commands/lanes-keep-alive-audit.d.ts +10 -0
- package/dist/commands/lanes-keep-alive-audit.js +19 -0
- package/dist/commands/lanes-message.js +37 -28
- package/dist/commands/lanes-shared.d.ts +1 -0
- package/dist/commands/lanes.js +33 -23
- package/dist/lib/core-utils/sentry-report.d.ts +3 -3
- package/dist/lib/core-utils/sentry-report.js +23 -2
- package/dist/lib/first-sync-next-step.d.ts +0 -3
- package/dist/lib/first-sync-next-step.js +6 -16
- package/dist/lib/lanes/autopilot.d.ts +13 -0
- package/dist/lib/lanes/autopilot.js +198 -37
- package/dist/lib/lanes/flag-keys.d.ts +7 -0
- package/dist/lib/lanes/flag-keys.js +7 -0
- package/dist/lib/lanes/keep-alive-audit.d.ts +46 -0
- package/dist/lib/lanes/keep-alive-audit.js +370 -0
- package/dist/lib/lanes/reconcile.js +20 -1
- package/dist/sentry.d.ts +8 -2
- package/dist/sentry.js +63 -6
- package/dist/utils/cli-telemetry.d.ts +4 -3
- package/dist/utils/cli-telemetry.js +5 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.315.0] — 2026-10-02
|
|
6
|
+
- The CLI loads the Sentry error-reporting library only when it first needs it, so commands that report nothing start with fewer imports. Reporting, scrubbing and flushing work as before.
|
|
7
|
+
- Keep first-sync guidance enabled without its rollout gate and remove the obsolete plan-limit fallback flag-off test.
|
|
8
|
+
|
|
9
|
+
- Gate autopilot nudge deduplication behind a default-off company flag, preserving message IDs and requiring a 30-minute active-condition cooldown for repeat nudges.
|
|
10
|
+
- Let lane autopilot skip malformed lead inputs individually behind a default-off flag and notify the affected lead once.
|
|
11
|
+
- Lead autopilot can batch stale KPI reminders into one oldest-first message per pass when the company enables `lanes.autopilot-kpi-staleness-batch`.
|
|
12
|
+
- Preserve the last policy tag when migrating trigger policies.
|
|
13
|
+
- Add read-only `hq lanes keep-alive-audit` reporting for lead keep-alive timing, early resumes, and missed restarts.
|
|
14
|
+
- Resolve bare lane IDs to their stored lane record before messaging or showing them, preventing messages from being written under unresolved IDs.
|
|
15
|
+
|
|
5
16
|
## [5.313.0] — 2026-10-02
|
|
6
17
|
- Gate live-only lane hire-grant counting by company; blocked and awaiting-input children no longer occupy slots when enabled, and their resume is checked against current capacity.
|
|
7
18
|
- Add company-gated lane low-disk admission protection and Codex rollout retention.
|
|
@@ -256,13 +256,18 @@ trigger_expr() {
|
|
|
256
256
|
|
|
257
257
|
# tag_tokens <raw-tags-line-without-brackets> — normalised, filtered tag tokens
|
|
258
258
|
# (one per line). vendor:x -> x; meta tags dropped; lowercased.
|
|
259
|
+
mpt_process_tag() {
|
|
260
|
+
local tag="$1"
|
|
261
|
+
tag="$(printf '%s' "$tag" | tr '[:upper:]' '[:lower:]' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')"
|
|
262
|
+
tag="${tag#vendor:}" # vendor:linear -> linear
|
|
263
|
+
[ -n "$tag" ] || return 0
|
|
264
|
+
case "$TAG_STOP" in *" $tag "*) return 0 ;; esac
|
|
265
|
+
printf '%s\n' "$tag"
|
|
266
|
+
}
|
|
267
|
+
|
|
259
268
|
tag_tokens() {
|
|
260
|
-
printf '%s' "$1" | tr ',' '\n' | while IFS= read -r tag; do
|
|
261
|
-
|
|
262
|
-
tag="${tag#vendor:}" # vendor:linear -> linear
|
|
263
|
-
[ -n "$tag" ] || continue
|
|
264
|
-
case "$TAG_STOP" in *" $tag "*) continue ;; esac
|
|
265
|
-
printf '%s\n' "$tag"
|
|
269
|
+
printf '%s' "$1" | tr ',' '\n' | while IFS= read -r tag || [ -n "$tag" ]; do
|
|
270
|
+
mpt_process_tag "$tag"
|
|
266
271
|
done
|
|
267
272
|
}
|
|
268
273
|
|
|
@@ -329,7 +334,7 @@ for dir in "${DIRS[@]}"; do
|
|
|
329
334
|
if policy_has_when "$f"; then skipped=$((skipped+1)); continue; fi
|
|
330
335
|
|
|
331
336
|
trig="$(fm trigger "$f")"
|
|
332
|
-
tags="$(fm tags "$f" | sed 's/^\[//; s/\]$//')"
|
|
337
|
+
tags="$(fm tags "$f" | sed 's/[[:space:]][[:space:]]*#.*$//; s/^\[//; s/\]$//')"
|
|
333
338
|
enforcement="$(fm enforcement "$f")"
|
|
334
339
|
if ! built="$(build_when "$trig" "$tags" "$enforcement")"; then
|
|
335
340
|
untriggered=$((untriggered+1))
|
|
@@ -8038,6 +8038,24 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
8038
8038
|
readonly description: "Print the KR freshness report as JSON";
|
|
8039
8039
|
}];
|
|
8040
8040
|
readonly subcommands: readonly [];
|
|
8041
|
+
}, {
|
|
8042
|
+
readonly name: "keep-alive-audit";
|
|
8043
|
+
readonly description: "Audit lead lane keep-alive timing";
|
|
8044
|
+
readonly aliases: readonly [];
|
|
8045
|
+
readonly hidden: false;
|
|
8046
|
+
readonly usage: "[options]";
|
|
8047
|
+
readonly arguments: readonly [];
|
|
8048
|
+
readonly options: readonly [{
|
|
8049
|
+
readonly flags: "--since <iso>";
|
|
8050
|
+
readonly description: "Include stopped turns at or after this ISO date-time";
|
|
8051
|
+
}, {
|
|
8052
|
+
readonly flags: "--lane <id>";
|
|
8053
|
+
readonly description: "Audit one exact lane id";
|
|
8054
|
+
}, {
|
|
8055
|
+
readonly flags: "--json";
|
|
8056
|
+
readonly description: "Print the report as JSON";
|
|
8057
|
+
}];
|
|
8058
|
+
readonly subcommands: readonly [];
|
|
8041
8059
|
}, {
|
|
8042
8060
|
readonly name: "autopilot";
|
|
8043
8061
|
readonly description: "Run lead autopilot checks and report follow-through metrics";
|
|
@@ -10346,6 +10346,29 @@ export const COMMAND_CATALOG = [
|
|
|
10346
10346
|
],
|
|
10347
10347
|
"subcommands": []
|
|
10348
10348
|
},
|
|
10349
|
+
{
|
|
10350
|
+
"name": "keep-alive-audit",
|
|
10351
|
+
"description": "Audit lead lane keep-alive timing",
|
|
10352
|
+
"aliases": [],
|
|
10353
|
+
"hidden": false,
|
|
10354
|
+
"usage": "[options]",
|
|
10355
|
+
"arguments": [],
|
|
10356
|
+
"options": [
|
|
10357
|
+
{
|
|
10358
|
+
"flags": "--since <iso>",
|
|
10359
|
+
"description": "Include stopped turns at or after this ISO date-time"
|
|
10360
|
+
},
|
|
10361
|
+
{
|
|
10362
|
+
"flags": "--lane <id>",
|
|
10363
|
+
"description": "Audit one exact lane id"
|
|
10364
|
+
},
|
|
10365
|
+
{
|
|
10366
|
+
"flags": "--json",
|
|
10367
|
+
"description": "Print the report as JSON"
|
|
10368
|
+
}
|
|
10369
|
+
],
|
|
10370
|
+
"subcommands": []
|
|
10371
|
+
},
|
|
10349
10372
|
{
|
|
10350
10373
|
"name": "autopilot",
|
|
10351
10374
|
"description": "Run lead autopilot checks and report follow-through metrics",
|
|
@@ -135,14 +135,10 @@ function deriveWhen(trigger, rawTags, enforcementRaw) {
|
|
|
135
135
|
seen.add(token);
|
|
136
136
|
terms.push(triggerExpr.includes(" ") ? `( ${triggerExpr} )` : triggerExpr);
|
|
137
137
|
}
|
|
138
|
-
const
|
|
138
|
+
const commentStrippedTags = rawTags.replace(/[ \t\v\f\r]+#.*$/, "");
|
|
139
|
+
const withoutBrackets = commentStrippedTags.replace(/^\[/, "").replace(/\]$/, "");
|
|
139
140
|
const rawTagParts = withoutBrackets.split(",");
|
|
140
|
-
for (
|
|
141
|
-
// tag_tokens pipes through `while read`; Bash drops the final tag when
|
|
142
|
-
// the source has no trailing comma/newline because that read returns EOF.
|
|
143
|
-
if (index === rawTagParts.length - 1 && !withoutBrackets.endsWith(","))
|
|
144
|
-
break;
|
|
145
|
-
const raw = rawTagParts[index];
|
|
141
|
+
for (const raw of rawTagParts) {
|
|
146
142
|
const tag = raw.toLowerCase().replace(/^\s+|\s+$/g, "").replace(/^vendor:/, "");
|
|
147
143
|
if (!tag || TAG_STOP.has(tag) || seen.has(tag))
|
|
148
144
|
continue;
|
|
@@ -174,7 +170,7 @@ function unreadablePolicyDiagnostic(filename, error) {
|
|
|
174
170
|
const message = code === "EACCES" || code === "EPERM" ? "Permission denied" : "No such file or directory";
|
|
175
171
|
const script = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../assets/scaffold/core/scripts/migrate-policy-triggers.sh");
|
|
176
172
|
const awk = `awk: fatal: cannot open file \`${filename}' for reading: ${message}\n`;
|
|
177
|
-
return `${script}: line
|
|
173
|
+
return `${script}: line 314: ${filename}: ${message}\n${awk.repeat(3)}`;
|
|
178
174
|
}
|
|
179
175
|
function shellGlobMd(fileSystem, directory) {
|
|
180
176
|
try {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
type WrapHandler = <Args extends unknown[]>(action: (...args: Args) => Promise<void>) => (...args: Args) => Promise<void>;
|
|
3
|
+
export interface LaneKeepAliveAuditCommandDeps {
|
|
4
|
+
hqRoot: () => string;
|
|
5
|
+
now: () => Date;
|
|
6
|
+
wrap: WrapHandler;
|
|
7
|
+
}
|
|
8
|
+
export declare function registerLaneKeepAliveAuditCommand(parent: Command, deps: LaneKeepAliveAuditCommandDeps): void;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=lanes-keep-alive-audit.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { buildKeepAliveAudit, formatKeepAliveAuditText } from "../lib/lanes/keep-alive-audit.js";
|
|
2
|
+
export function registerLaneKeepAliveAuditCommand(parent, deps) {
|
|
3
|
+
parent
|
|
4
|
+
.command("keep-alive-audit")
|
|
5
|
+
.description("Audit lead lane keep-alive timing")
|
|
6
|
+
.option("--since <iso>", "Include stopped turns at or after this ISO date-time")
|
|
7
|
+
.option("--lane <id>", "Audit one exact lane id")
|
|
8
|
+
.option("--json", "Print the report as JSON")
|
|
9
|
+
.action(deps.wrap(async (options) => {
|
|
10
|
+
const result = buildKeepAliveAudit({
|
|
11
|
+
hqRoot: deps.hqRoot(),
|
|
12
|
+
now: deps.now(),
|
|
13
|
+
...(options.since === undefined ? {} : { since: options.since }),
|
|
14
|
+
...(options.lane === undefined ? {} : { laneId: options.lane }),
|
|
15
|
+
});
|
|
16
|
+
console.log(options.json ? JSON.stringify(result) : formatKeepAliveAuditText(result));
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=lanes-keep-alive-audit.js.map
|
|
@@ -204,41 +204,37 @@ export function createLaneMessageHandlers(deps) {
|
|
|
204
204
|
}
|
|
205
205
|
async function runMessage(ctx, laneId, opts) {
|
|
206
206
|
const hqRoot = ctx.hqRoot();
|
|
207
|
-
const
|
|
208
|
-
? undefined
|
|
209
|
-
: (() => {
|
|
210
|
-
if (!ctx.isSsmOwnerDispatch()) {
|
|
211
|
-
throw new LanesError("hire_grant_dispatch_unauthorized", "Message grant context is only valid on an Outpost owner dispatch", { lane_id: laneId });
|
|
212
|
-
}
|
|
213
|
-
return parseOutpostHireGrantContext(opts.grantContext, laneId);
|
|
214
|
-
})();
|
|
207
|
+
const requestedLaneId = laneId;
|
|
215
208
|
let accepted = false;
|
|
216
209
|
let claimedMessages = [];
|
|
217
210
|
let storySwitchRollback;
|
|
218
211
|
let storySwitchDeliveryStarted = false;
|
|
212
|
+
const readMessageText = (targetLaneId) => {
|
|
213
|
+
if (opts.text !== undefined && opts.textFile !== undefined) {
|
|
214
|
+
throw new LanesError("message_text_source_conflict", "Pass either --text or --text-file, not both", { lane_id: targetLaneId });
|
|
215
|
+
}
|
|
216
|
+
let messageText = opts.text;
|
|
217
|
+
if (opts.textFile !== undefined) {
|
|
218
|
+
const textPath = path.isAbsolute(opts.textFile)
|
|
219
|
+
? opts.textFile
|
|
220
|
+
: path.resolve(ctx.callerCwd(), opts.textFile);
|
|
221
|
+
try {
|
|
222
|
+
messageText = ctx.readFileSync(textPath, "utf8");
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
throw new LanesError("message_text_file_unreadable", `Could not read message text file ${opts.textFile}: ${err instanceof Error ? err.message : String(err)}`, { lane_id: targetLaneId, path: textPath });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (messageText === undefined || messageText.length === 0) {
|
|
229
|
+
throw new LanesError("missing_text", "Pass non-empty message text with --text or --text-file", { lane_id: targetLaneId });
|
|
230
|
+
}
|
|
231
|
+
return messageText;
|
|
232
|
+
};
|
|
219
233
|
const markAccepted = () => {
|
|
220
234
|
accepted = true;
|
|
221
235
|
storySwitchDeliveryStarted = true;
|
|
222
236
|
opts.onAccepted?.();
|
|
223
237
|
};
|
|
224
|
-
if (opts.text !== undefined && opts.textFile !== undefined) {
|
|
225
|
-
throw new LanesError("message_text_source_conflict", "Pass either --text or --text-file, not both", { lane_id: laneId });
|
|
226
|
-
}
|
|
227
|
-
let text = opts.text;
|
|
228
|
-
if (opts.textFile !== undefined) {
|
|
229
|
-
const textPath = path.isAbsolute(opts.textFile)
|
|
230
|
-
? opts.textFile
|
|
231
|
-
: path.resolve(ctx.callerCwd(), opts.textFile);
|
|
232
|
-
try {
|
|
233
|
-
text = ctx.readFileSync(textPath, "utf8");
|
|
234
|
-
}
|
|
235
|
-
catch (err) {
|
|
236
|
-
throw new LanesError("message_text_file_unreadable", `Could not read message text file ${opts.textFile}: ${err instanceof Error ? err.message : String(err)}`, { lane_id: laneId, path: textPath });
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
if (text === undefined || text.length === 0) {
|
|
240
|
-
throw new LanesError("missing_text", "Pass non-empty message text with --text or --text-file", { lane_id: laneId });
|
|
241
|
-
}
|
|
242
238
|
let resolution;
|
|
243
239
|
try {
|
|
244
240
|
resolution = await deps.resolveCommandLane(ctx, laneId);
|
|
@@ -257,9 +253,20 @@ export function createLaneMessageHandlers(deps) {
|
|
|
257
253
|
primaryReportedMissing) {
|
|
258
254
|
throw err;
|
|
259
255
|
}
|
|
260
|
-
|
|
256
|
+
const text = readMessageText(requestedLaneId);
|
|
257
|
+
await dispatchPrimaryLaneMessage(ctx, requestedLaneId, { ...opts, text });
|
|
261
258
|
return;
|
|
262
259
|
}
|
|
260
|
+
laneId = resolution.lane.lane_id;
|
|
261
|
+
let text = readMessageText(laneId);
|
|
262
|
+
const hireGrantContext = opts.grantContext === undefined
|
|
263
|
+
? undefined
|
|
264
|
+
: (() => {
|
|
265
|
+
if (!ctx.isSsmOwnerDispatch()) {
|
|
266
|
+
throw new LanesError("hire_grant_dispatch_unauthorized", "Message grant context is only valid on an Outpost owner dispatch", { lane_id: laneId });
|
|
267
|
+
}
|
|
268
|
+
return parseOutpostHireGrantContext(opts.grantContext, laneId);
|
|
269
|
+
})();
|
|
263
270
|
let lane = resolution.lane;
|
|
264
271
|
if (resolution.remoteOnly || (lane.host.kind === "outpost" && !laneIsHostedHere(ctx, lane))) {
|
|
265
272
|
const fallbackAt = lane.state === "awaiting_input" ? ctx.now().toISOString() : undefined;
|
|
@@ -416,7 +423,9 @@ export function createLaneMessageHandlers(deps) {
|
|
|
416
423
|
let automaticAccountSelection = undefined;
|
|
417
424
|
if (opts.account === undefined && mechanism === "resume") {
|
|
418
425
|
try {
|
|
419
|
-
automaticAccountSelection = await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile
|
|
426
|
+
automaticAccountSelection = await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile, {
|
|
427
|
+
tolerateInvalidHostLaneLists: lane.host.kind === "local",
|
|
428
|
+
});
|
|
420
429
|
}
|
|
421
430
|
catch (err) {
|
|
422
431
|
const reasons = err instanceof LanesError && err.error === "claude_account_balance_no_eligible_account" &&
|
|
@@ -194,6 +194,7 @@ export interface LaneMessageHandlerDeps {
|
|
|
194
194
|
resolveResumeProfile(ctx: Ctx, lane: LaneRecord, workerId: string): ResolvedResumeProfile;
|
|
195
195
|
selectAutomaticClaudeResumeAccount(ctx: Ctx, lane: LaneRecord, workerProfile?: ResolvedResumeProfile, options?: {
|
|
196
196
|
repairUnavailableAccount?: boolean;
|
|
197
|
+
tolerateInvalidHostLaneLists?: boolean;
|
|
197
198
|
}): Promise<{
|
|
198
199
|
selection: LaneAccountSelection;
|
|
199
200
|
historyReason?: AccountHistoryEntry["reason"];
|
package/dist/commands/lanes.js
CHANGED
|
@@ -23,6 +23,7 @@ export { CLAUDE_ACCOUNT_BALANCE_SIGNALS_FLAG, LANE_OUTPOST_LOAD_BALANCING_FLAG }
|
|
|
23
23
|
export { resolveAnswerChoice } from "./lanes-questions.js";
|
|
24
24
|
import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
|
|
25
25
|
import { registerLaneKpisCommand } from "./lanes-kpis.js";
|
|
26
|
+
import { registerLaneKeepAliveAuditCommand } from "./lanes-keep-alive-audit.js";
|
|
26
27
|
import { registerLaneAutopilotReportCommand } from "./lanes-autopilot.js";
|
|
27
28
|
import { registerLaneChecksCommand } from "./lanes-checks.js";
|
|
28
29
|
import { registerLaneReportCommands } from "./lanes-report.js";
|
|
@@ -32,7 +33,7 @@ import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
|
32
33
|
import { withCliFeatureUsed } from "../utils/cli-telemetry.js";
|
|
33
34
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
34
35
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
35
|
-
import { readLanesRegistryFlag, readLanesRegistryFlagFailOff } from "../lib/lanes/registry-flag.js";
|
|
36
|
+
import { readLanesFlagLocalOverride, readLanesRegistryFlag, readLanesRegistryFlagFailOff } from "../lib/lanes/registry-flag.js";
|
|
36
37
|
import { LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG } from "../lib/lanes/flag-keys.js";
|
|
37
38
|
import { CODEX_ROLLOUT_RETENTION_FLAG, configuredCodexRolloutHomes, lowDiskGuardEnabledForLane, retainCodexRollouts } from "../lib/lanes/disk-guard.js";
|
|
38
39
|
import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
|
|
@@ -1549,10 +1550,10 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
|
|
|
1549
1550
|
}
|
|
1550
1551
|
const separator = laneId.lastIndexOf("_");
|
|
1551
1552
|
const hostSuffix = separator < 0 ? "" : laneId.slice(separator + 1);
|
|
1552
|
-
const
|
|
1553
|
+
const ownerLookupGate = !hostSuffix && requestedUlid !== undefined && options.allowHostlessOwnerLookup
|
|
1553
1554
|
? await options.allowHostlessOwnerLookup()
|
|
1554
|
-
:
|
|
1555
|
-
const hostlessUlidLookup =
|
|
1555
|
+
: false;
|
|
1556
|
+
const hostlessUlidLookup = ownerLookupGate !== false;
|
|
1556
1557
|
if ((!hostSuffix && !hostlessUlidLookup) ||
|
|
1557
1558
|
(hostSuffix && sanitizeHostComponent(hostSuffix) !== hostSuffix)) {
|
|
1558
1559
|
throw commandLaneNotFound(laneId, options.outpostHint === true);
|
|
@@ -1647,9 +1648,9 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
|
|
|
1647
1648
|
const laneCompany = hostlessUlidLookup
|
|
1648
1649
|
? resolveLaneCompany(ctx.hqRoot(), lane).company
|
|
1649
1650
|
: undefined;
|
|
1650
|
-
const companyMatches = !hostlessUlidLookup || (
|
|
1651
|
-
laneCompany
|
|
1652
|
-
|
|
1651
|
+
const companyMatches = !hostlessUlidLookup || ownerLookupGate === true || (ownerLookupGate === "check-company" &&
|
|
1652
|
+
laneCompany !== undefined && laneCompany !== null &&
|
|
1653
|
+
await options.allowHostlessOwnerLookup(laneCompany));
|
|
1653
1654
|
if (lane.host.kind === "local" && ((!hostlessUlidLookup && lane.lane_id === laneId &&
|
|
1654
1655
|
sanitizeHostComponent(lane.host.id) === hostSuffix) ||
|
|
1655
1656
|
(hostlessUlidLookup && laneUlidPart(lane.lane_id) === requestedUlid &&
|
|
@@ -1683,6 +1684,20 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
|
|
|
1683
1684
|
ownerHost: match.lane.host,
|
|
1684
1685
|
};
|
|
1685
1686
|
}
|
|
1687
|
+
async function resolveMessageLaneTarget(ctx, laneId) {
|
|
1688
|
+
return resolveCommandLaneTarget(ctx, laneId, {
|
|
1689
|
+
allowHostlessOwnerLookup: async (company) => {
|
|
1690
|
+
if (!company) {
|
|
1691
|
+
const localOverride = readLanesFlagLocalOverride(LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, ctx.env);
|
|
1692
|
+
return localOverride ?? "check-company";
|
|
1693
|
+
}
|
|
1694
|
+
return readLanesRegistryFlag(LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, {
|
|
1695
|
+
company,
|
|
1696
|
+
env: ctx.env,
|
|
1697
|
+
});
|
|
1698
|
+
},
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1686
1701
|
function laneUlidPart(laneId) {
|
|
1687
1702
|
const separator = laneId.indexOf("_");
|
|
1688
1703
|
if (separator !== -1 && separator !== 26)
|
|
@@ -1696,7 +1711,7 @@ function commandLaneNotFound(laneId, outpostHint, details = {}) {
|
|
|
1696
1711
|
: "";
|
|
1697
1712
|
return new LanesError("lane_not_found", `Lane ${laneId} was not found locally or on a registered Outpost with a matching host suffix${hint}`, { lane_id: laneId, ...details });
|
|
1698
1713
|
}
|
|
1699
|
-
async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
|
|
1714
|
+
async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config, options = {}) {
|
|
1700
1715
|
const localHost = laneHostFromCtx(ctx);
|
|
1701
1716
|
const localLanes = loadAllLanes(hqRoot);
|
|
1702
1717
|
const local = buildHostCapacitySnapshot({
|
|
@@ -1782,7 +1797,8 @@ async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
|
|
|
1782
1797
|
laneListAvailable = true;
|
|
1783
1798
|
}
|
|
1784
1799
|
catch (err) {
|
|
1785
|
-
if (isLanesError(err) && err.error === "outpost_lane_list_invalid"
|
|
1800
|
+
if (isLanesError(err) && err.error === "outpost_lane_list_invalid" &&
|
|
1801
|
+
options.tolerateInvalidLaneLists !== true)
|
|
1786
1802
|
throw err;
|
|
1787
1803
|
unavailableHosts.push({
|
|
1788
1804
|
host: { kind: "outpost", id },
|
|
@@ -5293,9 +5309,9 @@ async function refreshOutpostLaneCopiesForShow(ctx, lane, localLanes) {
|
|
|
5293
5309
|
const refreshedLane = refreshed[0] ?? null;
|
|
5294
5310
|
return { lane: refreshedLane ?? lane };
|
|
5295
5311
|
}
|
|
5296
|
-
async function runShow(ctx, laneId, hasPendingQuestionForLane) {
|
|
5312
|
+
async function runShow(ctx, laneId, hasPendingQuestionForLane, resolveLaneTarget) {
|
|
5297
5313
|
const hqRoot = ctx.hqRoot();
|
|
5298
|
-
const initialLane =
|
|
5314
|
+
const initialLane = (await resolveLaneTarget(ctx, laneId)).lane;
|
|
5299
5315
|
const initialLanes = loadAllLanes(hqRoot);
|
|
5300
5316
|
const showRefresh = await refreshOutpostLaneCopiesForShow(ctx, initialLane, initialLanes);
|
|
5301
5317
|
const storedLane = showRefresh.lane;
|
|
@@ -6598,7 +6614,9 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
|
|
|
6598
6614
|
const mayRebalance = BALANCE_MOVABLE_STATES.includes(lane.state);
|
|
6599
6615
|
if (!accountNeedsRepair && !mayRebalance)
|
|
6600
6616
|
return undefined;
|
|
6601
|
-
const balanceHosts = await loadClaudeBalanceHostSnapshots(ctx, hqRoot, config
|
|
6617
|
+
const balanceHosts = await loadClaudeBalanceHostSnapshots(ctx, hqRoot, config, {
|
|
6618
|
+
tolerateInvalidLaneLists: options.tolerateInvalidHostLaneLists === true,
|
|
6619
|
+
});
|
|
6602
6620
|
const targetHost = balanceHosts.snapshots.find((host) => host.capacity.host.kind === lane.host.kind && host.capacity.host.id === lane.host.id);
|
|
6603
6621
|
const currentTargetAccount = lane.account === undefined
|
|
6604
6622
|
? undefined
|
|
@@ -8205,16 +8223,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8205
8223
|
persistClaudeAccountSelection,
|
|
8206
8224
|
printJson,
|
|
8207
8225
|
recordResumeSetup,
|
|
8208
|
-
resolveCommandLane:
|
|
8209
|
-
allowHostlessOwnerLookup: async () => {
|
|
8210
|
-
const company = await commandCtx.resolveCompany();
|
|
8211
|
-
const enabled = await readLanesRegistryFlag(LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, {
|
|
8212
|
-
company,
|
|
8213
|
-
env: commandCtx.env,
|
|
8214
|
-
});
|
|
8215
|
-
return enabled ? company : undefined;
|
|
8216
|
-
},
|
|
8217
|
-
}),
|
|
8226
|
+
resolveCommandLane: resolveMessageLaneTarget,
|
|
8218
8227
|
resolveActiveQuestionCompany,
|
|
8219
8228
|
hasPendingQuestionForLane: questionHandlers.hasPendingQuestionForLane,
|
|
8220
8229
|
cancelFallbackQuestionsForLane: questionHandlers.cancelFallbackQuestionsForLane,
|
|
@@ -8310,6 +8319,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8310
8319
|
}, wrap);
|
|
8311
8320
|
registerOutpostPrunerCommand(lanes);
|
|
8312
8321
|
registerLaneKpisCommand(lanes, { hqRoot: ctx.hqRoot, now: ctx.now, wrap });
|
|
8322
|
+
registerLaneKeepAliveAuditCommand(lanes, { hqRoot: ctx.hqRoot, now: ctx.now, wrap });
|
|
8313
8323
|
registerLaneAutopilotReportCommand(lanes, {
|
|
8314
8324
|
hqRoot: ctx.hqRoot,
|
|
8315
8325
|
now: ctx.now,
|
|
@@ -8516,7 +8526,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8516
8526
|
.description("Show one lane")
|
|
8517
8527
|
.argument("<lane-id>", "Lane id")
|
|
8518
8528
|
.action(wrap(async (laneId) => {
|
|
8519
|
-
await runShow(ctx, laneId, questionHandlers.hasPendingQuestionForLane);
|
|
8529
|
+
await runShow(ctx, laneId, questionHandlers.hasPendingQuestionForLane, resolveMessageLaneTarget);
|
|
8520
8530
|
}));
|
|
8521
8531
|
const stories = lanes
|
|
8522
8532
|
.command("story")
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* scalar metadata is dropped instead: the allowlist, not a caller version
|
|
16
16
|
* match, determines what may leave the machine.
|
|
17
17
|
*/
|
|
18
|
-
import * as Sentry from "@sentry/node";
|
|
19
18
|
import type { CaptureContext, SeverityLevel } from "@sentry/node";
|
|
19
|
+
type SentryModule = typeof import("@sentry/node");
|
|
20
20
|
export declare const DEFAULT_SENTRY_REPORT_TIMEOUT_MS = 750;
|
|
21
21
|
export declare const MAX_SENTRY_REPORT_PAYLOAD_BYTES = 2048;
|
|
22
22
|
export declare const MAX_SENTRY_REPORT_METADATA_VALUE_BYTES = 256;
|
|
@@ -76,8 +76,8 @@ export type SentryReportOptions = {
|
|
|
76
76
|
export type SentryReportDependencies = {
|
|
77
77
|
sentry: {
|
|
78
78
|
captureMessage: (message: string, context?: CaptureContext | SeverityLevel) => string;
|
|
79
|
-
addBreadcrumb: (breadcrumb: Parameters<
|
|
80
|
-
metrics: Pick<
|
|
79
|
+
addBreadcrumb: (breadcrumb: Parameters<SentryModule["addBreadcrumb"]>[0]) => void;
|
|
80
|
+
metrics: Pick<SentryModule["metrics"], "count">;
|
|
81
81
|
flush: (timeout?: number) => PromiseLike<boolean>;
|
|
82
82
|
};
|
|
83
83
|
stdout: Pick<NodeJS.WriteStream, "write">;
|
|
@@ -19,12 +19,18 @@ import { createHash } from "node:crypto";
|
|
|
19
19
|
import { mkdirSync, readdirSync, readFileSync, renameSync, rmdirSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
20
20
|
import { tmpdir } from "node:os";
|
|
21
21
|
import { dirname, join } from "node:path";
|
|
22
|
-
import
|
|
22
|
+
import { createRequire } from "node:module";
|
|
23
23
|
import { gte as semverGte, valid as validSemver } from "semver";
|
|
24
24
|
import { CLI_VERSION } from "../../cli-version.js";
|
|
25
25
|
import { beforeSend } from "../../sentry-before-send.js";
|
|
26
26
|
import { resolveLiveRoot } from "../../utils/hq-roots.js";
|
|
27
27
|
import { hookTimeoutDebugContextEnabled } from "./hook-timeout-debug-context-flag.js";
|
|
28
|
+
const requireSentry = createRequire(import.meta.url);
|
|
29
|
+
let sentryModule;
|
|
30
|
+
function loadSentryModule() {
|
|
31
|
+
sentryModule ??= requireSentry("@sentry/node");
|
|
32
|
+
return sentryModule;
|
|
33
|
+
}
|
|
28
34
|
export const DEFAULT_SENTRY_REPORT_TIMEOUT_MS = 750;
|
|
29
35
|
const MAX_SENTRY_REPORT_TIMEOUT_MS = 5_000;
|
|
30
36
|
export const MAX_SENTRY_REPORT_PAYLOAD_BYTES = 2_048;
|
|
@@ -176,7 +182,22 @@ const SENTRY_REPORT_METADATA_KEY_HEADROOM = 2;
|
|
|
176
182
|
// while unknown keys are deliberately dropped.
|
|
177
183
|
export const MAX_SENTRY_REPORT_METADATA_KEYS = ALLOWED_SENTRY_REPORT_METADATA_KEYS.length + SENTRY_REPORT_METADATA_KEY_HEADROOM;
|
|
178
184
|
const defaultDependencies = {
|
|
179
|
-
sentry:
|
|
185
|
+
sentry: {
|
|
186
|
+
captureMessage(message, context) {
|
|
187
|
+
return loadSentryModule().captureMessage(message, context);
|
|
188
|
+
},
|
|
189
|
+
addBreadcrumb(breadcrumb) {
|
|
190
|
+
loadSentryModule().addBreadcrumb(breadcrumb);
|
|
191
|
+
},
|
|
192
|
+
metrics: {
|
|
193
|
+
count(name, value, options) {
|
|
194
|
+
loadSentryModule().metrics.count(name, value, options);
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
flush(timeout) {
|
|
198
|
+
return loadSentryModule().flush(timeout);
|
|
199
|
+
},
|
|
200
|
+
},
|
|
180
201
|
stdout: process.stdout,
|
|
181
202
|
stderr: process.stderr,
|
|
182
203
|
timeoutDebugContextEnabled: (signal) => hookTimeoutDebugContextEnabled({}, signal),
|
|
@@ -3,13 +3,11 @@ export declare const FIRST_SYNC_NEXT_STEP_COPY = "Save a file in your HQ folder,
|
|
|
3
3
|
export declare const FIRST_SYNC_CONFIRMATION_COPY = "Your first work sync is complete.";
|
|
4
4
|
export type FirstSyncStepOutcome = "shown" | "followed";
|
|
5
5
|
export declare function shouldShowFirstSyncNextStep(input: {
|
|
6
|
-
flagEnabled: boolean;
|
|
7
6
|
interactive: boolean;
|
|
8
7
|
alreadyShown: boolean;
|
|
9
8
|
alreadyCompleted: boolean;
|
|
10
9
|
}): boolean;
|
|
11
10
|
export declare function shouldConfirmFirstWorkBearingSync(input: {
|
|
12
|
-
flagEnabled: boolean;
|
|
13
11
|
interactive: boolean;
|
|
14
12
|
alreadyCompleted: boolean;
|
|
15
13
|
workBearing: boolean;
|
|
@@ -27,7 +25,6 @@ export declare function firstSyncStepMarkerPaths(identity: string, home?: string
|
|
|
27
25
|
};
|
|
28
26
|
export declare function claimFirstSyncStepMarker(kind: "shown" | "completed", identity: string, home?: string): boolean;
|
|
29
27
|
export declare function firstSyncStepTelemetryProperties(outcome: FirstSyncStepOutcome): Record<string, unknown>;
|
|
30
|
-
export declare function isFirstSyncNextStepEnabled(): boolean;
|
|
31
28
|
/** Called once after the first completed CLI action, before process finalization. */
|
|
32
29
|
export declare function maybeShowFirstSyncNextStep(): boolean;
|
|
33
30
|
/** Capture the first positive CLI sync result without changing transfer logic. */
|
|
@@ -2,7 +2,6 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
-
import { resolveProcessFlagGate } from "./flag-registry.js";
|
|
6
5
|
import { cognitoSubjectKey, isMachineIdentity, loadCachedTokens, } from "../utils/cognito-session.js";
|
|
7
6
|
export const CLI_FIRST_SYNC_NEXT_STEP_FLAG = "cli.first-sync-next-step";
|
|
8
7
|
export const FIRST_SYNC_NEXT_STEP_COPY = "Save a file in your HQ folder, then run `hq sync push`.";
|
|
@@ -17,14 +16,12 @@ const AUTOMATION_ENV_MARKERS = [
|
|
|
17
16
|
];
|
|
18
17
|
const pendingOutcomes = [];
|
|
19
18
|
export function shouldShowFirstSyncNextStep(input) {
|
|
20
|
-
return (input.
|
|
21
|
-
input.interactive &&
|
|
19
|
+
return (input.interactive &&
|
|
22
20
|
!input.alreadyShown &&
|
|
23
21
|
!input.alreadyCompleted);
|
|
24
22
|
}
|
|
25
23
|
export function shouldConfirmFirstWorkBearingSync(input) {
|
|
26
|
-
return (input.
|
|
27
|
-
input.interactive &&
|
|
24
|
+
return (input.interactive &&
|
|
28
25
|
!input.alreadyCompleted &&
|
|
29
26
|
input.workBearing);
|
|
30
27
|
}
|
|
@@ -111,9 +108,6 @@ export function firstSyncStepTelemetryProperties(outcome) {
|
|
|
111
108
|
flagKey: CLI_FIRST_SYNC_NEXT_STEP_FLAG,
|
|
112
109
|
};
|
|
113
110
|
}
|
|
114
|
-
export function isFirstSyncNextStepEnabled() {
|
|
115
|
-
return resolveProcessFlagGate(CLI_FIRST_SYNC_NEXT_STEP_FLAG, { fallback: false }, () => false);
|
|
116
|
-
}
|
|
117
111
|
function currentPersonIdentity() {
|
|
118
112
|
return cognitoSubjectKey(loadCachedTokens());
|
|
119
113
|
}
|
|
@@ -131,15 +125,13 @@ function queueOutcome(outcome) {
|
|
|
131
125
|
}
|
|
132
126
|
/** Called once after the first completed CLI action, before process finalization. */
|
|
133
127
|
export function maybeShowFirstSyncNextStep() {
|
|
134
|
-
// Keep the cheap eligibility
|
|
135
|
-
|
|
136
|
-
if (!isCurrentSessionEligible() || !isFirstSyncNextStepEnabled())
|
|
128
|
+
// Keep the cheap eligibility check ahead of identity and filesystem reads.
|
|
129
|
+
if (!isCurrentSessionEligible())
|
|
137
130
|
return false;
|
|
138
131
|
const identity = currentPersonIdentity();
|
|
139
132
|
if (!identity)
|
|
140
133
|
return false;
|
|
141
134
|
const shouldShow = shouldShowFirstSyncNextStep({
|
|
142
|
-
flagEnabled: true,
|
|
143
135
|
interactive: true,
|
|
144
136
|
alreadyShown: firstSyncStepMarkerExists("shown", identity),
|
|
145
137
|
alreadyCompleted: firstSyncStepMarkerExists("completed", identity),
|
|
@@ -165,13 +157,11 @@ export function recordFirstWorkBearingSync(result) {
|
|
|
165
157
|
const alreadyCompleted = firstSyncStepMarkerExists("completed", identity);
|
|
166
158
|
if (alreadyCompleted || !claimFirstSyncStepMarker("completed", identity))
|
|
167
159
|
return false;
|
|
168
|
-
// Persist completion
|
|
169
|
-
//
|
|
170
|
-
// other non-interactive human syncs count too; only the message is TTY-gated.
|
|
160
|
+
// Persist completion for non-interactive human syncs too; only the message is
|
|
161
|
+
// TTY-gated.
|
|
171
162
|
if (!isCurrentSessionEligible())
|
|
172
163
|
return false;
|
|
173
164
|
if (!shouldConfirmFirstWorkBearingSync({
|
|
174
|
-
flagEnabled: isFirstSyncNextStepEnabled(),
|
|
175
165
|
interactive: true,
|
|
176
166
|
alreadyCompleted: false,
|
|
177
167
|
workBearing: true,
|
|
@@ -5,10 +5,16 @@ export declare const LEAD_AUTOPILOT_DEFAULT_ENABLED = false;
|
|
|
5
5
|
export declare const LEAD_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG = "lanes.autopilot-missed-deadline-escalation";
|
|
6
6
|
export declare const LEAD_AUTOPILOT_MISSED_DEADLINE_ESCALATION_DEFAULT_ENABLED = false;
|
|
7
7
|
export declare const LEAD_AUTOPILOT_MISSED_DEADLINE_ESCALATION_DELAY_MS: number;
|
|
8
|
+
export declare const LEAD_AUTOPILOT_NUDGE_DEDUPE_FLAG = "lanes.autopilot-nudge-dedupe";
|
|
9
|
+
export declare const LEAD_AUTOPILOT_NUDGE_DEDUPE_DEFAULT_ENABLED = false;
|
|
10
|
+
export declare const LEAD_AUTOPILOT_NUDGE_REDELIVERY_DELAY_MS: number;
|
|
11
|
+
export declare const LEAD_AUTOPILOT_NUDGE_STATE_RETENTION_MS: number;
|
|
8
12
|
export declare const LEAD_AUTOPILOT_REVIEW_DELAY_MS: number;
|
|
9
13
|
export declare const LEAD_AUTOPILOT_FILL_DELAY_MS: number;
|
|
10
14
|
export declare const LEAD_AUTOPILOT_PROMPT_LOOP = "personal/knowledge/team-lead-operating-loop.md";
|
|
11
15
|
export declare const LEAD_AUTOPILOT_KPI_STALE_AFTER_MINUTES = 55;
|
|
16
|
+
export declare const LEAD_AUTOPILOT_PER_LEAD_ERRORS_FLAG = "lanes.autopilot-per-lead-errors";
|
|
17
|
+
export declare const LEAD_AUTOPILOT_PER_LEAD_ERRORS_DEFAULT_ENABLED = false;
|
|
12
18
|
export interface RegisteredLead {
|
|
13
19
|
team: string;
|
|
14
20
|
lane: string;
|
|
@@ -74,6 +80,11 @@ export interface AutopilotPassResult {
|
|
|
74
80
|
skipped: Array<{
|
|
75
81
|
lead_lane: string;
|
|
76
82
|
reason: string;
|
|
83
|
+
} | {
|
|
84
|
+
lane_id: string;
|
|
85
|
+
error: string;
|
|
86
|
+
path: string;
|
|
87
|
+
message: string;
|
|
77
88
|
}>;
|
|
78
89
|
kpi_status?: AutopilotKpiStatus[];
|
|
79
90
|
/** Set on an Outpost that has not received a lead registry from the primary yet. */
|
|
@@ -97,6 +108,7 @@ export declare function planLeadAutopilotWakes(input: {
|
|
|
97
108
|
managerStatus?: Record<string, unknown>;
|
|
98
109
|
keepAliveResumed?: boolean;
|
|
99
110
|
kpiStalenessEnabled?: boolean;
|
|
111
|
+
kpiStalenessBatchEnabled?: boolean;
|
|
100
112
|
}): AutopilotWake[];
|
|
101
113
|
export declare function runLeadAutopilotPass(input: {
|
|
102
114
|
hqRoot: string;
|
|
@@ -106,6 +118,7 @@ export declare function runLeadAutopilotPass(input: {
|
|
|
106
118
|
now: Date;
|
|
107
119
|
dryRun?: boolean;
|
|
108
120
|
readFlag?: typeof readLanesRegistryFlag;
|
|
121
|
+
readNudgeDedupeFlag?: typeof readLanesRegistryFlag;
|
|
109
122
|
readDeadlineFlag?: typeof readLanesRegistryFlag;
|
|
110
123
|
keepAliveResumedLeads?: ReadonlySet<string>;
|
|
111
124
|
missedDeadlineEscalationDelayMs?: number;
|