@indigoai-us/hq-cli 5.345.44 → 5.345.45
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 +9 -0
- package/dist/command-catalog.generated.d.ts +17 -1
- package/dist/command-catalog.generated.js +21 -1
- package/dist/commands/access.js +6 -4
- package/dist/commands/bot.js +1 -1
- package/dist/commands/db-destroy.js +5 -4
- package/dist/commands/lanes-message.js +84 -51
- package/dist/commands/lanes-resume.js +44 -0
- package/dist/commands/lanes-shared.d.ts +25 -0
- package/dist/commands/lanes.d.ts +4 -0
- package/dist/commands/lanes.js +213 -4
- package/dist/commands/meetings.js +4 -1
- package/dist/lib/flag-snapshot-file.js +33 -25
- package/dist/lib/lanes/flag-keys.d.ts +1 -0
- package/dist/lib/lanes/flag-keys.js +1 -0
- package/dist/lib/lanes/focus-guard.d.ts +65 -0
- package/dist/lib/lanes/focus-guard.js +91 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +1 -0
- package/dist/lib/lanes/outpost-dispatch.js +1 -0
- package/dist/lib/lanes/store.js +29 -0
- package/dist/lib/lanes/story-ledger.d.ts +14 -0
- package/dist/lib/lanes/story-ledger.js +46 -0
- package/dist/lib/lanes/types.d.ts +10 -2
- package/dist/lib/lanes/types.js +4 -3
- package/dist/utils/package-use-lease.d.ts +1 -0
- package/dist/utils/package-use-lease.js +29 -0
- package/dist/utils/self-update-backoff.d.ts +17 -0
- package/dist/utils/self-update-backoff.js +73 -0
- package/dist/utils/self-update.d.ts +7 -0
- package/dist/utils/self-update.js +36 -0
- package/dist/utils/vault-api.d.ts +33 -1
- package/dist/utils/vault-api.js +55 -5
- package/dist/utils/version-gate.d.ts +13 -0
- package/dist/utils/version-gate.js +35 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.45] — 2026-10-07
|
|
6
|
+
- A failed automatic CLI update now pauses automatic updates for 24 hours, including for newer releases, instead of retrying on every command. `hq rescue` still updates on request. Processes started by an update can no longer start another install, and update package-manager processes run with Corepack settings that cannot change your default pnpm version.
|
|
7
|
+
|
|
8
|
+
- Hosted agents can run `hq access`, `hq secrets --personal`, and the `hq db destroy` role check under their own `agt_*` identity instead of failing with "No person entity found". Person-only commands (`hq meetings import`, `hq bot create`) now tell an agent identity that the command is not available to agents.
|
|
9
|
+
- Add a default-off focus guard to lane creation and story-switch resume, with logged, auditable overrides.
|
|
10
|
+
- `hq lanes set <lane> --company <slug>` binds missing company attribution, including on legacy and terminal lane records.
|
|
11
|
+
- Use the per-user local application cache when Windows denies the configured flag snapshot cache.
|
|
12
|
+
|
|
5
13
|
## [5.345.44] — 2026-10-07
|
|
14
|
+
- Long-running CLI processes now refresh their package-use lease so desktop updates only reclaim holders whose liveness heartbeat has expired.
|
|
6
15
|
- Quota-exhausted lanes can fail over within their provider, hold with per-account eligibility reasons when none is available, and retry when the earliest quota reset arrives. The default-off `lanes.quota-account-failover` gate controls the new recovery path.
|
|
7
16
|
|
|
8
17
|
- When a company's plan does not include remote databases, `hq db provision`, `usage`, `sql`, `dump` and `destroy` now show which plan includes them, the $10 per database app price and the upgrade link.
|
|
@@ -11469,6 +11469,10 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
11469
11469
|
readonly flags: "--story <id>";
|
|
11470
11470
|
readonly description: "Story id from the project's prd.json";
|
|
11471
11471
|
readonly hidden: false;
|
|
11472
|
+
}, {
|
|
11473
|
+
readonly flags: "--focus-override <reason>";
|
|
11474
|
+
readonly description: "Human-approved reason to override the lane focus guard";
|
|
11475
|
+
readonly hidden: false;
|
|
11472
11476
|
}, {
|
|
11473
11477
|
readonly flags: "--no-story <reason>";
|
|
11474
11478
|
readonly description: "Create a lane without a story and record why";
|
|
@@ -11683,7 +11687,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
11683
11687
|
readonly subcommands: readonly [];
|
|
11684
11688
|
}, {
|
|
11685
11689
|
readonly name: "set";
|
|
11686
|
-
readonly description: "Change
|
|
11690
|
+
readonly description: "Change lane settings or bind its missing company attribution";
|
|
11687
11691
|
readonly aliases: readonly [];
|
|
11688
11692
|
readonly hidden: false;
|
|
11689
11693
|
readonly helpOption: true;
|
|
@@ -11697,6 +11701,10 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
11697
11701
|
readonly description: "Lane id";
|
|
11698
11702
|
}];
|
|
11699
11703
|
readonly options: readonly [{
|
|
11704
|
+
readonly flags: "--company <slug>";
|
|
11705
|
+
readonly description: "Bind missing company attribution to this lane";
|
|
11706
|
+
readonly hidden: false;
|
|
11707
|
+
}, {
|
|
11700
11708
|
readonly flags: "--model <model>";
|
|
11701
11709
|
readonly description: "Model for the lane's next and later turns";
|
|
11702
11710
|
readonly hidden: false;
|
|
@@ -12374,6 +12382,10 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
12374
12382
|
readonly flags: "--story <id>";
|
|
12375
12383
|
readonly description: "Re-point story attribution; a changed story is named in the resumed prompt";
|
|
12376
12384
|
readonly hidden: false;
|
|
12385
|
+
}, {
|
|
12386
|
+
readonly flags: "--focus-override <reason>";
|
|
12387
|
+
readonly description: "Human-approved reason to override the lane focus guard";
|
|
12388
|
+
readonly hidden: false;
|
|
12377
12389
|
}, {
|
|
12378
12390
|
readonly flags: "--account <id>";
|
|
12379
12391
|
readonly description: "Move the lane's session to this account of its provider before the message resumes it";
|
|
@@ -12497,6 +12509,10 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
12497
12509
|
readonly flags: "--story <id>";
|
|
12498
12510
|
readonly description: "Re-point story attribution; a changed story is named in the resumed prompt";
|
|
12499
12511
|
readonly hidden: false;
|
|
12512
|
+
}, {
|
|
12513
|
+
readonly flags: "--focus-override <reason>";
|
|
12514
|
+
readonly description: "Human-approved reason to override the lane focus guard";
|
|
12515
|
+
readonly hidden: false;
|
|
12500
12516
|
}, {
|
|
12501
12517
|
readonly flags: "--account <id>";
|
|
12502
12518
|
readonly description: "Move the lane's session to this account of its provider before resuming";
|
|
@@ -13932,6 +13932,11 @@ export const COMMAND_CATALOG = [
|
|
|
13932
13932
|
"description": "Story id from the project's prd.json",
|
|
13933
13933
|
"hidden": false
|
|
13934
13934
|
},
|
|
13935
|
+
{
|
|
13936
|
+
"flags": "--focus-override <reason>",
|
|
13937
|
+
"description": "Human-approved reason to override the lane focus guard",
|
|
13938
|
+
"hidden": false
|
|
13939
|
+
},
|
|
13935
13940
|
{
|
|
13936
13941
|
"flags": "--no-story <reason>",
|
|
13937
13942
|
"description": "Create a lane without a story and record why",
|
|
@@ -14198,7 +14203,7 @@ export const COMMAND_CATALOG = [
|
|
|
14198
14203
|
},
|
|
14199
14204
|
{
|
|
14200
14205
|
"name": "set",
|
|
14201
|
-
"description": "Change
|
|
14206
|
+
"description": "Change lane settings or bind its missing company attribution",
|
|
14202
14207
|
"aliases": [],
|
|
14203
14208
|
"hidden": false,
|
|
14204
14209
|
"helpOption": true,
|
|
@@ -14214,6 +14219,11 @@ export const COMMAND_CATALOG = [
|
|
|
14214
14219
|
}
|
|
14215
14220
|
],
|
|
14216
14221
|
"options": [
|
|
14222
|
+
{
|
|
14223
|
+
"flags": "--company <slug>",
|
|
14224
|
+
"description": "Bind missing company attribution to this lane",
|
|
14225
|
+
"hidden": false
|
|
14226
|
+
},
|
|
14217
14227
|
{
|
|
14218
14228
|
"flags": "--model <model>",
|
|
14219
14229
|
"description": "Model for the lane's next and later turns",
|
|
@@ -15045,6 +15055,11 @@ export const COMMAND_CATALOG = [
|
|
|
15045
15055
|
"description": "Re-point story attribution; a changed story is named in the resumed prompt",
|
|
15046
15056
|
"hidden": false
|
|
15047
15057
|
},
|
|
15058
|
+
{
|
|
15059
|
+
"flags": "--focus-override <reason>",
|
|
15060
|
+
"description": "Human-approved reason to override the lane focus guard",
|
|
15061
|
+
"hidden": false
|
|
15062
|
+
},
|
|
15048
15063
|
{
|
|
15049
15064
|
"flags": "--account <id>",
|
|
15050
15065
|
"description": "Move the lane's session to this account of its provider before the message resumes it",
|
|
@@ -15197,6 +15212,11 @@ export const COMMAND_CATALOG = [
|
|
|
15197
15212
|
"description": "Re-point story attribution; a changed story is named in the resumed prompt",
|
|
15198
15213
|
"hidden": false
|
|
15199
15214
|
},
|
|
15215
|
+
{
|
|
15216
|
+
"flags": "--focus-override <reason>",
|
|
15217
|
+
"description": "Human-approved reason to override the lane focus guard",
|
|
15218
|
+
"hidden": false
|
|
15219
|
+
},
|
|
15200
15220
|
{
|
|
15201
15221
|
"flags": "--account <id>",
|
|
15202
15222
|
"description": "Move the lane's session to this account of its provider before resuming",
|
package/dist/commands/access.js
CHANGED
|
@@ -19,7 +19,7 @@ import * as path from "node:path";
|
|
|
19
19
|
import * as readline from "node:readline";
|
|
20
20
|
import { ListObjectsV2Command, } from "@aws-sdk/client-s3";
|
|
21
21
|
import { DEFAULT_HQ_ROOT } from "../utils/cognito-session.js";
|
|
22
|
-
import { getCompanyUid, vaultApiFetch,
|
|
22
|
+
import { getCompanyUid, vaultApiFetch, resolveCallerUid } from "../utils/vault-api.js";
|
|
23
23
|
import { parseCompanySlugFromPath, resolveBrowseSession, makeCompanyPresignFactory, runGet, } from "./files-browse.js";
|
|
24
24
|
import { listJournals, syncDoctor, } from "@indigoai-us/hq-cloud";
|
|
25
25
|
import { listActiveMembers } from "./members.js";
|
|
@@ -705,9 +705,11 @@ export async function runAccessForPath(path, opts) {
|
|
|
705
705
|
}));
|
|
706
706
|
};
|
|
707
707
|
const members = await listMembers();
|
|
708
|
-
const
|
|
709
|
-
const me = members.find((m) => m.personUid ===
|
|
710
|
-
|
|
708
|
+
const caller = await resolveCallerUid(token);
|
|
709
|
+
const me = members.find((m) => m.personUid === caller.uid);
|
|
710
|
+
// An agent has no mailbox and no person entity. `hq files share --with`
|
|
711
|
+
// accepts its agt_* uid, so the access request names the agent by uid.
|
|
712
|
+
let requesterEmail = caller.kind === "agent" ? caller.uid : me?.personEmail;
|
|
711
713
|
const requesterName = me?.personName;
|
|
712
714
|
if (!requesterEmail) {
|
|
713
715
|
const claims = peekIdToken(token);
|
package/dist/commands/bot.js
CHANGED
|
@@ -504,7 +504,7 @@ export async function runBotCreate(nameArg, opts) {
|
|
|
504
504
|
throw Object.assign(new Error(`The name ${RESERVED_SETUP_BOT_NAME} is reserved for HQ's setup bot; create it with --worker ${RESERVED_SETUP_BOT_NAME}.`), { expected: true });
|
|
505
505
|
}
|
|
506
506
|
const token = await ownerToken();
|
|
507
|
-
const owner = await resolveCallerPersonUid(token);
|
|
507
|
+
const owner = await resolveCallerPersonUid(token, undefined, { command: "hq bot create" });
|
|
508
508
|
if (!owner)
|
|
509
509
|
throw Object.assign(new Error("Your HQ login has no person entity yet; open the HQ desktop app once, then retry."), { expected: true });
|
|
510
510
|
const api = new BotApi({ token: async () => token });
|
|
@@ -12,16 +12,17 @@ async function defaultCheckRole(deps, companyUid) {
|
|
|
12
12
|
if (process.env.HQ_DB_MOCK_CONTROL_PLANE === "1")
|
|
13
13
|
return "unknown";
|
|
14
14
|
try {
|
|
15
|
-
const [{ listActiveMembers }, {
|
|
15
|
+
const [{ listActiveMembers }, { resolveCallerUid }] = await Promise.all([
|
|
16
16
|
import("./members.js"),
|
|
17
17
|
import("../utils/vault-api.js"),
|
|
18
18
|
]);
|
|
19
19
|
const token = await deps.getAccessToken();
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// An agent's membership row carries its agt_* uid in the personUid slot.
|
|
21
|
+
const [caller, members] = await Promise.all([
|
|
22
|
+
resolveCallerUid(token),
|
|
22
23
|
listActiveMembers(token, companyUid),
|
|
23
24
|
]);
|
|
24
|
-
const me = members.find((m) => m.personUid ===
|
|
25
|
+
const me = members.find((m) => m.personUid === caller.uid);
|
|
25
26
|
if (!me)
|
|
26
27
|
return "unknown";
|
|
27
28
|
return me.role === "owner" || me.role === "admin" ? "allowed" : "denied";
|
|
@@ -14,6 +14,8 @@ import { laneRunDir, probePidRecord, readPidRecord, readProcStarttime, spawnLane
|
|
|
14
14
|
import { findRetiredLaneEntry, keepAliveExclusion, keepAliveSupersededBy, LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_FLAG, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds } from "../lib/lanes/keep-alive.js";
|
|
15
15
|
import { isTerminalLaneState } from "../lib/lanes/types.js";
|
|
16
16
|
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
17
|
+
import { appendLaneFocusOverride, focusGuardRefusal, makeFocusOverride } from "../lib/lanes/focus-guard.js";
|
|
18
|
+
import { appendProjectFocusOverride, removeProjectFocusOverride } from "../lib/lanes/story-ledger.js";
|
|
17
19
|
import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
|
|
18
20
|
import { accountMoveSummary, accountRequestKeepsCurrentLane, assertCurrentResumeAccountAvailable, callerMatchesSessionSenior, recordedResumeEffort, resetKeepAliveProgress, resolveResumeEffort } from "./lanes-shared.js";
|
|
19
21
|
export function parseMonitorLaneMessagePayload(input) {
|
|
@@ -381,6 +383,28 @@ export function createLaneMessageHandlers(deps) {
|
|
|
381
383
|
if (opts.story !== undefined && !opts.story.trim()) {
|
|
382
384
|
throw new LanesError("missing_story", "--story must be non-empty", {});
|
|
383
385
|
}
|
|
386
|
+
if ((opts.project !== undefined || opts.story !== undefined) && ctx.storyLedgerEnabled()) {
|
|
387
|
+
validateProjectStories(hqRoot, projectId, [storyId], lane.company?.slug);
|
|
388
|
+
}
|
|
389
|
+
if (opts.focusOverride !== undefined && !storyId) {
|
|
390
|
+
throw new LanesError("focus_override_story_required", "--focus-override requires --story", { lane_id: laneId });
|
|
391
|
+
}
|
|
392
|
+
const attributionChanged = projectId !== lane.project_id || storyId !== lane.story_id;
|
|
393
|
+
const focusAssessment = opts.project !== undefined || opts.story !== undefined || opts.focusOverride !== undefined
|
|
394
|
+
? await deps.focusGuardAssessment(ctx, {
|
|
395
|
+
projectId,
|
|
396
|
+
storyIds: storyId ? [storyId] : [],
|
|
397
|
+
company: lane.company,
|
|
398
|
+
checkTeamBlockers: attributionChanged,
|
|
399
|
+
overrideReason: opts.focusOverride,
|
|
400
|
+
})
|
|
401
|
+
: { enabled: false, reasons: [] };
|
|
402
|
+
if (focusAssessment.enabled && focusAssessment.reasons.length > 0 && opts.focusOverride === undefined) {
|
|
403
|
+
throw focusGuardRefusal(focusAssessment.reasons);
|
|
404
|
+
}
|
|
405
|
+
const focusOverride = focusAssessment.enabled && opts.focusOverride !== undefined
|
|
406
|
+
? makeFocusOverride({ reason: opts.focusOverride, laneId, at: ctx.now().toISOString(), storyId: storyId })
|
|
407
|
+
: undefined;
|
|
384
408
|
const refreshProfileSettings = lane.state === "done" &&
|
|
385
409
|
(projectId !== lane.project_id || storyId !== lane.story_id) &&
|
|
386
410
|
await deps.reuseProfileRefreshEnabled(ctx, lane);
|
|
@@ -567,12 +591,6 @@ export function createLaneMessageHandlers(deps) {
|
|
|
567
591
|
});
|
|
568
592
|
const messageId = mintMessageId();
|
|
569
593
|
const at = ctx.now().toISOString();
|
|
570
|
-
if (opts.story !== undefined && !opts.story.trim()) {
|
|
571
|
-
throw new LanesError("missing_story", "--story must be non-empty", {});
|
|
572
|
-
}
|
|
573
|
-
if ((opts.project !== undefined || opts.story !== undefined) && ctx.storyLedgerEnabled()) {
|
|
574
|
-
validateProjectStories(hqRoot, projectId, [storyId], lane.company?.slug);
|
|
575
|
-
}
|
|
576
594
|
const notice = storySwitchNotice(lane.project_id, lane.story_id, projectId, storyId);
|
|
577
595
|
const promptedText = notice === undefined ? text : `${notice}\n\n${text}`;
|
|
578
596
|
const framedMonitorEvent = opts.monitorEvent === undefined
|
|
@@ -602,51 +620,66 @@ export function createLaneMessageHandlers(deps) {
|
|
|
602
620
|
sequence: opts.monitorEvent.sequence,
|
|
603
621
|
}),
|
|
604
622
|
};
|
|
605
|
-
if (
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
623
|
+
if (attributionChanged || focusOverride !== undefined) {
|
|
624
|
+
if (focusOverride !== undefined) {
|
|
625
|
+
appendProjectFocusOverride(hqRoot, projectId, lane.company?.slug, focusOverride);
|
|
626
|
+
}
|
|
627
|
+
try {
|
|
628
|
+
lane = casUpdateLane(hqRoot, laneId, (current) => {
|
|
629
|
+
let next = current;
|
|
630
|
+
if (attributionChanged) {
|
|
631
|
+
const already = current.attribution_history.some((entry) => entry.message_id === messageId);
|
|
632
|
+
if (!already) {
|
|
633
|
+
const existingStory = laneStory(current, projectId, storyId);
|
|
634
|
+
storySwitchRollback = {
|
|
635
|
+
message_id: messageId,
|
|
636
|
+
project_id: current.project_id,
|
|
637
|
+
story_id: current.story_id,
|
|
638
|
+
no_story_reason: current.no_story_reason,
|
|
639
|
+
...(!existingStory ? {
|
|
640
|
+
added_story: { project_id: projectId, story_id: storyId, status: "in_progress", added_at: at, updated_at: at },
|
|
641
|
+
} : {}),
|
|
642
|
+
story_switch_unacknowledged: current.story_switch_unacknowledged,
|
|
643
|
+
};
|
|
644
|
+
next = {
|
|
645
|
+
...current,
|
|
646
|
+
project_id: projectId,
|
|
647
|
+
story_id: storyId,
|
|
648
|
+
no_story_reason: storyId ? undefined : current.no_story_reason,
|
|
649
|
+
stories: laneStory(current, projectId, storyId)
|
|
650
|
+
? laneStories(current)
|
|
651
|
+
: [
|
|
652
|
+
...laneStories(current),
|
|
653
|
+
{ project_id: projectId, story_id: storyId, status: "in_progress", added_at: at, updated_at: at },
|
|
654
|
+
],
|
|
655
|
+
attribution_history: appendCappedHistory(current.attribution_history, {
|
|
656
|
+
project_id: projectId,
|
|
657
|
+
story_id: storyId,
|
|
658
|
+
message_id: messageId,
|
|
659
|
+
at,
|
|
660
|
+
}),
|
|
661
|
+
story_switch_pending: {
|
|
662
|
+
previous_project_id: current.project_id,
|
|
663
|
+
previous_story_id: current.story_id,
|
|
664
|
+
project_id: projectId,
|
|
665
|
+
story_id: storyId,
|
|
666
|
+
message_id: messageId,
|
|
667
|
+
at,
|
|
668
|
+
previous_artifact_keys: artifactIdentityKeys(current.last_envelope?.artifacts),
|
|
669
|
+
},
|
|
670
|
+
story_switch_unacknowledged: undefined,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return focusOverride === undefined ? next : appendLaneFocusOverride(next, focusOverride);
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
catch (error) {
|
|
678
|
+
if (focusOverride !== undefined) {
|
|
679
|
+
removeProjectFocusOverride(hqRoot, projectId, lane.company?.slug, focusOverride);
|
|
680
|
+
}
|
|
681
|
+
throw error;
|
|
682
|
+
}
|
|
650
683
|
}
|
|
651
684
|
let resumed = null;
|
|
652
685
|
let move = null;
|
|
@@ -7,6 +7,8 @@ import { assertNoSecretText } from "../lib/lanes/text-scan.js";
|
|
|
7
7
|
import { laneStories, laneStory, storyWarning } from "../lib/lanes/stories.js";
|
|
8
8
|
import { artifactIdentityKeys, rollbackStorySwitchAttribution, storySwitchNotice } from "../lib/lanes/story-switch.js";
|
|
9
9
|
import { markSpawnFailure, spawnLaneWorker } from "../lib/lanes/spawn.js";
|
|
10
|
+
import { appendProjectFocusOverride, removeProjectFocusOverride } from "../lib/lanes/story-ledger.js";
|
|
11
|
+
import { appendLaneFocusOverride, makeFocusOverride } from "../lib/lanes/focus-guard.js";
|
|
10
12
|
import { accountMoveSummary, accountRequestKeepsCurrentLane, assertCurrentResumeAccountAvailable, hasKeepAliveProgress, recordedResumeEffort, resetKeepAliveProgress, resolveResumeEffort } from "./lanes-shared.js";
|
|
11
13
|
export function createLaneResumeHandlers(deps) {
|
|
12
14
|
const { archiveResumeMessage, assertLaneRunsHereForAdmission, assertLaneRunsHereForOperatorMutation, assessLaneAdmission, automaticBalanceMoveHistoryReason, authorizeFailedResume, authorizeResumeProfileRepin, casUpdateLane, dispatchOutpostLaneCommand, laneIsHostedHere, laneWithRecoveredProviderSession, mintMessageId, persistClaudeAccountSelection, prepareAccountMove, printJson, recordResumeSetup, resolveResumeProfile, selectAutomaticClaudeResumeAccount, validateResumeProfileModel, withResumeHireAdmission } = deps;
|
|
@@ -200,10 +202,22 @@ export function createLaneResumeHandlers(deps) {
|
|
|
200
202
|
if (opts.story !== undefined && !opts.story.trim()) {
|
|
201
203
|
throw new LanesError("missing_story", "--story must be non-empty", {});
|
|
202
204
|
}
|
|
205
|
+
if (opts.focusOverride !== undefined && !storyId) {
|
|
206
|
+
throw new LanesError("focus_override_story_required", "--focus-override requires a target story", { lane_id: laneId });
|
|
207
|
+
}
|
|
203
208
|
if ((opts.project !== undefined || opts.story !== undefined) && ctx.storyLedgerEnabled()) {
|
|
204
209
|
validateProjectStories(hqRoot, projectId, [storyId], lane.company?.slug);
|
|
205
210
|
}
|
|
206
211
|
const attributionChanged = projectId !== lane.project_id || storyId !== lane.story_id;
|
|
212
|
+
const focusAssessment = opts.story !== undefined || opts.project !== undefined || opts.focusOverride !== undefined
|
|
213
|
+
? await deps.focusGuardAssessment(ctx, {
|
|
214
|
+
projectId,
|
|
215
|
+
storyIds: storyId ? [storyId] : [],
|
|
216
|
+
company: lane.company,
|
|
217
|
+
checkTeamBlockers: attributionChanged,
|
|
218
|
+
overrideReason: opts.focusOverride,
|
|
219
|
+
})
|
|
220
|
+
: { enabled: false, reasons: [] };
|
|
207
221
|
const refreshProfileSettings = lane.state === "done" && attributionChanged &&
|
|
208
222
|
await deps.reuseProfileRefreshEnabled(ctx, lane);
|
|
209
223
|
const refreshWorker = opts.worker ?? (refreshProfileSettings ? lane.worker_profile : undefined);
|
|
@@ -370,6 +384,36 @@ export function createLaneResumeHandlers(deps) {
|
|
|
370
384
|
});
|
|
371
385
|
}
|
|
372
386
|
try {
|
|
387
|
+
if (focusAssessment.enabled && opts.focusOverride !== undefined) {
|
|
388
|
+
const focusOverride = makeFocusOverride({
|
|
389
|
+
reason: opts.focusOverride,
|
|
390
|
+
laneId: lane.lane_id,
|
|
391
|
+
at,
|
|
392
|
+
storyId,
|
|
393
|
+
});
|
|
394
|
+
try {
|
|
395
|
+
appendProjectFocusOverride(hqRoot, projectId, lane.company?.slug, focusOverride);
|
|
396
|
+
lane = casUpdateLane(hqRoot, laneId, (current) => ({
|
|
397
|
+
...appendLaneFocusOverride(current, focusOverride),
|
|
398
|
+
}));
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
try {
|
|
402
|
+
removeProjectFocusOverride(hqRoot, projectId, lane.company?.slug, focusOverride);
|
|
403
|
+
}
|
|
404
|
+
catch (cleanupError) {
|
|
405
|
+
process.stderr.write(`${JSON.stringify({
|
|
406
|
+
event: "lanes.focus_guard.override_rollback_failed",
|
|
407
|
+
lane_id: lane.lane_id,
|
|
408
|
+
error: cleanupError instanceof Error ? cleanupError.name : "UnknownError",
|
|
409
|
+
})}\n`);
|
|
410
|
+
}
|
|
411
|
+
throw new LanesError("focus_override_persist_failed", "Could not persist the focus override; lane resume was refused", {
|
|
412
|
+
lane_id: lane.lane_id,
|
|
413
|
+
error: error instanceof Error ? error.name : "UnknownError",
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
373
417
|
let move = null;
|
|
374
418
|
if (opts.account !== undefined && !keepCurrentAccount) {
|
|
375
419
|
move = await prepareAccountMove(ctx, lane, {
|
|
@@ -3,6 +3,7 @@ import type { OutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
|
3
3
|
import type { OutpostDispatchResult } from "../lib/lanes/outpost-dispatch.js";
|
|
4
4
|
import type { CpuBusySource } from "../lib/lanes/throttle.js";
|
|
5
5
|
import type { ProbeCommand } from "../lib/lanes/models.js";
|
|
6
|
+
import type { FocusGuardReason } from "../lib/lanes/focus-guard.js";
|
|
6
7
|
import type { KillProcessFn, ProveDetachedFn, SpawnProcessFn } from "../lib/lanes/spawn.js";
|
|
7
8
|
import type { WatcherLoopDeps } from "../lib/lanes/watcher.js";
|
|
8
9
|
import type { RunCompactionFn } from "../lib/lanes/compaction.js";
|
|
@@ -38,6 +39,7 @@ export interface LaneMessageOptions {
|
|
|
38
39
|
textFile?: string;
|
|
39
40
|
project?: string;
|
|
40
41
|
story?: string;
|
|
42
|
+
focusOverride?: string;
|
|
41
43
|
compact?: boolean;
|
|
42
44
|
account?: string;
|
|
43
45
|
model?: string;
|
|
@@ -68,6 +70,7 @@ export interface LaneResumeOptions {
|
|
|
68
70
|
failedResumeContext?: string;
|
|
69
71
|
project?: string;
|
|
70
72
|
story?: string;
|
|
73
|
+
focusOverride?: string;
|
|
71
74
|
compact?: boolean;
|
|
72
75
|
account?: string;
|
|
73
76
|
model?: string;
|
|
@@ -181,6 +184,16 @@ export interface LaneMessageHandlerDeps {
|
|
|
181
184
|
casUpdateLane(hqRoot: string, laneId: string, updater: (current: LaneRecord) => LaneRecord): LaneRecord;
|
|
182
185
|
dispatchOutpostLaneCommand(ctx: Ctx, lane: LaneRecord, action: "message" | "resume", options: LaneMessageOptions | LaneResumeOptions, behavior?: OutpostLaneDispatchBehavior): Promise<LaneMessageResponse | undefined>;
|
|
183
186
|
dispatchPrimaryLaneMessage(ctx: Ctx, laneId: string, options: LaneMessageOptions): Promise<LaneMessageResponse | undefined>;
|
|
187
|
+
focusGuardAssessment(ctx: Ctx, input: {
|
|
188
|
+
projectId: string;
|
|
189
|
+
storyIds: readonly string[];
|
|
190
|
+
company: LaneCompanyRef | null;
|
|
191
|
+
checkTeamBlockers: boolean;
|
|
192
|
+
overrideReason?: string;
|
|
193
|
+
}): Promise<{
|
|
194
|
+
enabled: boolean;
|
|
195
|
+
reasons: FocusGuardReason[];
|
|
196
|
+
}>;
|
|
184
197
|
drainHookStatusFor(ctx: Ctx, lane: LaneRecord): {
|
|
185
198
|
registered: boolean;
|
|
186
199
|
kind: string;
|
|
@@ -217,6 +230,16 @@ export interface LaneMessageHandlerDeps {
|
|
|
217
230
|
withResumeHireAdmission<T>(ctx: Ctx, lane: LaneRecord, fn: () => T, grantContext?: OutpostHireGrantContext): Promise<T>;
|
|
218
231
|
}
|
|
219
232
|
export interface LaneResumeHandlerDeps extends Pick<LaneMessageHandlerDeps, "onLaneWorkerStarted" | "archiveResumeMessage" | "assertLaneRunsHereForAdmission" | "assertLaneRunsHereForOperatorMutation" | "assessLaneAdmission" | "casUpdateLane" | "laneIsHostedHere" | "laneWithRecoveredProviderSession" | "mintMessageId" | "prepareAccountMove" | "persistClaudeAccountSelection" | "printJson" | "recordResumeSetup" | "resolveCommandLane" | "hasPendingQuestionForLane" | "pendingQuestionsForLane" | "cancelFallbackQuestionsForLane" | "resolveResumeProfile" | "reuseProfileRefreshEnabled" | "selectAutomaticClaudeResumeAccount" | "validateResumeProfileModel" | "withResumeHireAdmission"> {
|
|
233
|
+
focusGuardAssessment(ctx: Ctx, input: {
|
|
234
|
+
projectId: string;
|
|
235
|
+
storyIds: readonly string[];
|
|
236
|
+
company: LaneCompanyRef | null;
|
|
237
|
+
checkTeamBlockers: boolean;
|
|
238
|
+
overrideReason?: string;
|
|
239
|
+
}): Promise<{
|
|
240
|
+
enabled: boolean;
|
|
241
|
+
reasons: FocusGuardReason[];
|
|
242
|
+
}>;
|
|
220
243
|
dispatchOutpostLaneCommand(ctx: Ctx, lane: LaneRecord, action: "resume", options: LaneResumeOptions, behavior?: OutpostLaneDispatchBehavior): Promise<void>;
|
|
221
244
|
authorizeFailedResume(ctx: Ctx, lane: LaneRecord, encodedContext?: string): string;
|
|
222
245
|
authorizeResumeProfileRepin(ctx: Ctx, lane: LaneRecord, profile: ResolvedResumeProfile | undefined, encodedContext: string | undefined): ResumeProfileRepinAuthorization | undefined;
|
|
@@ -285,6 +308,8 @@ export interface Ctx {
|
|
|
285
308
|
readLeadAutopilotFlag: (flag: string, options: {
|
|
286
309
|
company: LaneCompanyRef;
|
|
287
310
|
}) => Promise<boolean>;
|
|
311
|
+
/** Test seam for the default-off project focus guard flag. */
|
|
312
|
+
focusGuardEnabled?: (company?: LaneCompanyRef) => Promise<boolean>;
|
|
288
313
|
storyLedgerEnabled: () => boolean;
|
|
289
314
|
isSsmOwnerDispatch: () => boolean;
|
|
290
315
|
withGrantHireLocks: typeof withLaneHireLocks;
|
package/dist/commands/lanes.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { type LanesMesh, type OutpostRouteIdentity, type SpawnLoginResult } from
|
|
|
11
11
|
export type { LanesMesh, SpawnLoginResult } from "./lanes-shared.js";
|
|
12
12
|
export { CLAUDE_ACCOUNT_BALANCE_SIGNALS_FLAG, LANE_OUTPOST_LOAD_BALANCING_FLAG } from "./lanes-shared.js";
|
|
13
13
|
export { resolveAnswerChoice } from "./lanes-questions.js";
|
|
14
|
+
import { type FocusGuardPullRequest } from "../lib/lanes/focus-guard.js";
|
|
14
15
|
import type { OutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
15
16
|
export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
16
17
|
export type { OutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
@@ -23,6 +24,7 @@ import type { WatcherLoopDeps } from "../lib/lanes/watcher.js";
|
|
|
23
24
|
import { type KnownModelsDeps, type ProbeCommand } from "../lib/lanes/models.js";
|
|
24
25
|
import { spawnLaneWorker, type KillProcessFn, type ProveDetachedFn, type SpawnProcessFn } from "../lib/lanes/spawn.js";
|
|
25
26
|
export { spawnLaneWorker };
|
|
27
|
+
export declare function openPullRequestsForProject(hqRoot: string, metadata: Record<string, unknown>): FocusGuardPullRequest[];
|
|
26
28
|
export { questionBodyAbsPath };
|
|
27
29
|
export { MAX_LANE_DEPTH };
|
|
28
30
|
export declare const LANES_RUNS_REL: string;
|
|
@@ -98,6 +100,8 @@ export interface LanesCommandDeps {
|
|
|
98
100
|
lanePlacementEnabled?: (company: LaneCompanyRef | undefined) => Promise<boolean>;
|
|
99
101
|
/** Test seam for the default-off story ledger flag. */
|
|
100
102
|
storyLedgerEnabled?: () => boolean;
|
|
103
|
+
/** Test seam for the default-off focus guard flag. */
|
|
104
|
+
focusGuardEnabled?: (company?: LaneCompanyRef) => Promise<boolean>;
|
|
101
105
|
/** Test seam for the SSM-only Outpost owner grant path. */
|
|
102
106
|
isSsmOwnerDispatch?: () => boolean;
|
|
103
107
|
/** Test seam for changing a lane between pre-lock and locked grant reads. */
|