@indigoai-us/hq-cli 5.345.43 → 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 +18 -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/db-remote-deps.js +4 -0
- package/dist/commands/lanes-checks.d.ts +1 -0
- package/dist/commands/lanes-checks.js +6 -1
- package/dist/commands/lanes-message.js +84 -51
- package/dist/commands/lanes-report.d.ts +2 -1
- package/dist/commands/lanes-report.js +63 -6
- 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 +228 -9
- package/dist/commands/meetings.js +4 -1
- package/dist/index.js +43 -7
- package/dist/lib/core-utils/sentry-report.js +8 -1
- package/dist/lib/db/control-plane.d.ts +5 -0
- package/dist/lib/db/control-plane.js +7 -0
- package/dist/lib/flag-snapshot-file.js +33 -25
- package/dist/lib/lanes/flag-keys.d.ts +4 -0
- package/dist/lib/lanes/flag-keys.js +4 -0
- package/dist/lib/lanes/focus-guard.d.ts +65 -0
- package/dist/lib/lanes/focus-guard.js +91 -0
- package/dist/lib/lanes/grants.js +1 -1
- package/dist/lib/lanes/outpost-dispatch.d.ts +1 -0
- package/dist/lib/lanes/outpost-dispatch.js +1 -0
- package/dist/lib/lanes/reconcile.d.ts +2 -0
- package/dist/lib/lanes/reconcile.js +39 -1
- package/dist/lib/lanes/spawn.d.ts +4 -0
- package/dist/lib/lanes/spawn.js +132 -6
- package/dist/lib/lanes/store.js +64 -0
- package/dist/lib/lanes/story-ledger.d.ts +14 -0
- package/dist/lib/lanes/story-ledger.js +46 -0
- package/dist/lib/lanes/tracker-report.d.ts +46 -0
- package/dist/lib/lanes/tracker-report.js +85 -1
- package/dist/lib/lanes/types.d.ts +12 -2
- package/dist/lib/lanes/types.js +5 -3
- package/dist/main.js +5 -1
- package/dist/startup-registration.d.ts +3 -0
- package/dist/startup-registration.js +11 -4
- 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,6 +2,24 @@
|
|
|
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
|
+
|
|
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.
|
|
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.
|
|
16
|
+
|
|
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.
|
|
18
|
+
- `hq lanes report` can show a gated focus list, focus-guard override reasons, and per-team 24-hour focus share.
|
|
19
|
+
- Return a child's hire grant to its senior when the lane becomes terminal, let the senior adjust or clear the saved grant, report grant updates correctly, and reject over-cap increases while allowing reductions that repair oversubscription.
|
|
20
|
+
- Recover CLI startup when a package update temporarily removes a module from the installed command graph.
|
|
21
|
+
|
|
22
|
+
|
|
5
23
|
## [5.345.43] — 2026-10-07
|
|
6
24
|
- The published core-command E2E workflow now retries the exact npm release install for up to 10 minutes while registry propagation completes.
|
|
7
25
|
|
|
@@ -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";
|
|
@@ -42,6 +42,10 @@ export function describeRemoteError(error) {
|
|
|
42
42
|
const status = error.status;
|
|
43
43
|
const code = error.code;
|
|
44
44
|
if (code === "REMOTE_DB_NOT_AVAILABLE" || status === 501) {
|
|
45
|
+
const reason = error.reason;
|
|
46
|
+
const message = error.message;
|
|
47
|
+
if (reason === "plan" && typeof message === "string" && message)
|
|
48
|
+
return message;
|
|
45
49
|
return "Remote databases are not available yet.";
|
|
46
50
|
}
|
|
47
51
|
if (status === 402 || code === "PLAN_REQUIRED") {
|
|
@@ -22,6 +22,7 @@ export declare function mappingIncludesStory(mapping: ProjectMapping, storyId: s
|
|
|
22
22
|
export interface ProjectReadResult {
|
|
23
23
|
stories: Array<Record<string, unknown>>;
|
|
24
24
|
mtime: number;
|
|
25
|
+
focus_overrides?: unknown;
|
|
25
26
|
errors?: Array<{
|
|
26
27
|
path: string;
|
|
27
28
|
field: string;
|
|
@@ -272,7 +272,12 @@ export function readProject(root, company, prdPath, tolerantStories = false) {
|
|
|
272
272
|
return [story];
|
|
273
273
|
});
|
|
274
274
|
try {
|
|
275
|
-
return {
|
|
275
|
+
return {
|
|
276
|
+
stories,
|
|
277
|
+
mtime: fs.statSync(prdPath).mtimeMs,
|
|
278
|
+
...(parsed.focus_overrides === undefined ? {} : { focus_overrides: parsed.focus_overrides }),
|
|
279
|
+
...(tolerantStories ? { errors } : {}),
|
|
280
|
+
};
|
|
276
281
|
}
|
|
277
282
|
catch (err) {
|
|
278
283
|
return fail("tracker_checks_input_unreadable", `Project task list could not be stat'ed: ${err instanceof Error ? err.message : String(err)}`, { path: prdPath });
|
|
@@ -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;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import { type QuestionBody } from "../lib/lanes/questions.js";
|
|
3
|
-
import type { QuestionRecord } from "../lib/lanes/types.js";
|
|
3
|
+
import type { LaneCompanyRef, QuestionRecord } from "../lib/lanes/types.js";
|
|
4
4
|
import { type LaneChecksDeps } from "./lanes-checks.js";
|
|
5
5
|
type WrapHandler = <Args extends unknown[]>(action: (...args: Args) => Promise<void>) => (...args: Args) => Promise<void>;
|
|
6
6
|
export interface LaneReportsDeps extends LaneChecksDeps {
|
|
7
7
|
readQuestionBody: (root: string, question: QuestionRecord) => QuestionBody;
|
|
8
|
+
focusDashboardEnabled: (company: LaneCompanyRef) => Promise<boolean>;
|
|
8
9
|
}
|
|
9
10
|
export declare function registerLaneReportCommands(parent: Command, deps: LaneReportsDeps, wrap: WrapHandler): void;
|
|
10
11
|
export {};
|
|
@@ -163,7 +163,7 @@ function issueFromError(err, fallbackPath, fallbackField, ids = {}) {
|
|
|
163
163
|
...(typeof details.question_id === "string" ? { question_id: details.question_id } : ids.question_id ? { question_id: ids.question_id } : {}),
|
|
164
164
|
};
|
|
165
165
|
}
|
|
166
|
-
async function teamInput(root, company, project, questions, deps, errors = []) {
|
|
166
|
+
async function teamInput(root, company, project, questions, deps, errors = [], includeFocusDashboard = false) {
|
|
167
167
|
const projectDirectory = path.join(root, "companies", company.slug, "projects", project.projectId);
|
|
168
168
|
const prdPath = path.join(projectDirectory, "prd.json");
|
|
169
169
|
let prd = { stories: [], mtime: 0 };
|
|
@@ -213,10 +213,64 @@ async function teamInput(root, company, project, questions, deps, errors = []) {
|
|
|
213
213
|
}
|
|
214
214
|
normalizedUpdatedAt = normalized;
|
|
215
215
|
}
|
|
216
|
+
const ownerValue = includeFocusDashboard ? story.owner ?? story.assignee : undefined;
|
|
217
|
+
let owner = null;
|
|
218
|
+
if (ownerValue !== undefined && ownerValue !== null) {
|
|
219
|
+
if (typeof ownerValue === "string" && ownerValue.trim())
|
|
220
|
+
owner = ownerValue.trim();
|
|
221
|
+
else
|
|
222
|
+
errors.push({ path: prdPath, field: "owner", story_id: id, message: "Story owner must be a non-empty string when present" });
|
|
223
|
+
}
|
|
224
|
+
const deadlineValue = includeFocusDashboard ? story.deadline ?? story.due_at ?? story.due_date : undefined;
|
|
225
|
+
let deadline = null;
|
|
226
|
+
if (deadlineValue !== undefined && deadlineValue !== null) {
|
|
227
|
+
if (typeof deadlineValue === "string" && deadlineValue.trim())
|
|
228
|
+
deadline = deadlineValue.trim();
|
|
229
|
+
else
|
|
230
|
+
errors.push({ path: prdPath, field: "deadline", story_id: id, message: "Story deadline must be a non-empty string when present" });
|
|
231
|
+
}
|
|
216
232
|
const refs = storyReferences(story, prdPath, id, errors);
|
|
217
233
|
pullRequests.push(...refs.pullRequests);
|
|
218
234
|
releases.push(...refs.releases);
|
|
219
|
-
stories.push({
|
|
235
|
+
stories.push({
|
|
236
|
+
id,
|
|
237
|
+
title,
|
|
238
|
+
status,
|
|
239
|
+
focus_id: story.focus_id,
|
|
240
|
+
kpi: story.kpi,
|
|
241
|
+
owner,
|
|
242
|
+
deadline,
|
|
243
|
+
references: refs.references,
|
|
244
|
+
...(normalizedUpdatedAt ? { updated_at: normalizedUpdatedAt } : {}),
|
|
245
|
+
source_path: prdPath,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
const focusOverrides = [];
|
|
249
|
+
if (includeFocusDashboard && prd.focus_overrides !== undefined) {
|
|
250
|
+
if (!Array.isArray(prd.focus_overrides)) {
|
|
251
|
+
errors.push({ path: prdPath, field: "focus_overrides", message: "Project focus_overrides must be an array" });
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
for (const [index, raw] of prd.focus_overrides.entries()) {
|
|
255
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
256
|
+
errors.push({ path: prdPath, field: `focus_overrides[${index}]`, message: "Focus override must be an object" });
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const record = raw;
|
|
260
|
+
if (["reason", "by_lane", "at", "story"].some((field) => typeof record[field] !== "string" || !record[field].trim())) {
|
|
261
|
+
errors.push({ path: prdPath, field: `focus_overrides[${index}]`, message: "Focus override needs non-empty reason, by_lane, at, and story fields" });
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const at = normalizeIsoTimestamp(record.at);
|
|
265
|
+
if (at === null) {
|
|
266
|
+
errors.push({ path: prdPath, field: `focus_overrides[${index}].at`, message: "Focus override at must be a valid timestamp" });
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const override = { reason: record.reason, by_lane: record.by_lane, at, story: record.story };
|
|
270
|
+
if (mappingIncludesStory(project, override.story))
|
|
271
|
+
focusOverrides.push(override);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
220
274
|
}
|
|
221
275
|
const teamQuestions = [];
|
|
222
276
|
for (const record of questions.filter((question) => question.project_id === project.projectId
|
|
@@ -236,6 +290,7 @@ async function teamInput(root, company, project, questions, deps, errors = []) {
|
|
|
236
290
|
team: project.team,
|
|
237
291
|
project_id: project.projectId,
|
|
238
292
|
stories,
|
|
293
|
+
focus_overrides: focusOverrides,
|
|
239
294
|
pull_requests: pullRequests,
|
|
240
295
|
releases,
|
|
241
296
|
blockers: await trackerRecords(root, company, project, "blocker", errors),
|
|
@@ -246,7 +301,7 @@ async function teamInput(root, company, project, questions, deps, errors = []) {
|
|
|
246
301
|
errors,
|
|
247
302
|
};
|
|
248
303
|
}
|
|
249
|
-
async function allTeamInputs(root, company, deps) {
|
|
304
|
+
async function allTeamInputs(root, company, deps, includeFocusDashboard) {
|
|
250
305
|
const mappings = loadProjectMappings(root, company.slug);
|
|
251
306
|
const teams = [];
|
|
252
307
|
for (const project of [...mappings.values()].sort((a, b) => a.team.localeCompare(b.team) || a.projectId.localeCompare(b.projectId))) {
|
|
@@ -258,7 +313,7 @@ async function allTeamInputs(root, company, deps) {
|
|
|
258
313
|
catch (err) {
|
|
259
314
|
errors.push(issueFromError(err, path.join(root, "companies", company.slug, "projects", project.projectId), "questions"));
|
|
260
315
|
}
|
|
261
|
-
teams.push(await teamInput(root, company, project, questions, deps, errors));
|
|
316
|
+
teams.push(await teamInput(root, company, project, questions, deps, errors, includeFocusDashboard));
|
|
262
317
|
}
|
|
263
318
|
return teams;
|
|
264
319
|
}
|
|
@@ -282,6 +337,7 @@ export function registerLaneReportCommands(parent, deps, wrap) {
|
|
|
282
337
|
const company = await resolveTrackerCompany(deps, options.company);
|
|
283
338
|
const root = deps.hqRoot();
|
|
284
339
|
const generatedAt = deps.now().toISOString();
|
|
340
|
+
const focusDashboardEnabled = await deps.focusDashboardEnabled(company);
|
|
285
341
|
const previous = options.since === undefined ? readTrackerReportState(root, company.slug) : null;
|
|
286
342
|
const since = options.since ?? previous?.last_report_at ?? FIRST_TRACKER_REPORT_SINCE;
|
|
287
343
|
const input = {
|
|
@@ -289,7 +345,8 @@ export function registerLaneReportCommands(parent, deps, wrap) {
|
|
|
289
345
|
generated_at: generatedAt,
|
|
290
346
|
since,
|
|
291
347
|
since_source: options.since !== undefined ? "explicit" : previous ? "previous_report" : "first_run",
|
|
292
|
-
|
|
348
|
+
focus_dashboard_enabled: focusDashboardEnabled,
|
|
349
|
+
teams: await allTeamInputs(root, company, deps, focusDashboardEnabled),
|
|
293
350
|
};
|
|
294
351
|
const report = buildTrackerOwnerReport(input);
|
|
295
352
|
const hasErrors = report.teams.some((team) => team.errors.length > 0);
|
|
@@ -323,7 +380,7 @@ export function registerLaneReportCommands(parent, deps, wrap) {
|
|
|
323
380
|
catch (err) {
|
|
324
381
|
errors.push(issueFromError(err, path.join(root, "companies", company.slug, "projects", candidates[0].projectId), "questions"));
|
|
325
382
|
}
|
|
326
|
-
const input = await teamInput(root, company, candidates[0], questions, deps, errors);
|
|
383
|
+
const input = await teamInput(root, company, candidates[0], questions, deps, errors, false);
|
|
327
384
|
const page = buildTrackerTeamPage(input, company.slug, since, generatedAt);
|
|
328
385
|
if (page.errors.length > 0)
|
|
329
386
|
process.exitCode = 2;
|
|
@@ -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, {
|