@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
|
@@ -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. */
|
package/dist/commands/lanes.js
CHANGED
|
@@ -37,7 +37,9 @@ import { laneDepthCap } from "../lib/lanes/depth-cap.js";
|
|
|
37
37
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
38
38
|
import { runResidentCommandWithPackageLease } from "../utils/resident-package-preload.js";
|
|
39
39
|
import { readLanesFlagLocalOverride, readLanesRegistryFlag, readLanesRegistryFlagFailOff } from "../lib/lanes/registry-flag.js";
|
|
40
|
-
import {
|
|
40
|
+
import { appendProjectFocusOverride, readProjectStoryFocusData, removeProjectFocusOverride } from "../lib/lanes/story-ledger.js";
|
|
41
|
+
import { appendLaneFocusOverride, assertFocusGuardAllows, makeFocusOverride } from "../lib/lanes/focus-guard.js";
|
|
42
|
+
import { LANE_FOCUS_GUARD_FLAG, LANE_FOCUS_DASHBOARD_FLAG, LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG, LANE_REUSE_REFRESHES_PROFILE_FLAG, } from "../lib/lanes/flag-keys.js";
|
|
41
43
|
import { configuredCodexRolloutHomes, retainCodexRollouts } from "../lib/lanes/disk-guard.js";
|
|
42
44
|
import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
|
|
43
45
|
import { runLeadAutopilotPass } from "../lib/lanes/autopilot.js";
|
|
@@ -86,6 +88,133 @@ import { defaultProbe, validateProfileLaneModel, validateLaneModel, } from "../l
|
|
|
86
88
|
import { parseCliModel, refuseOutpostSelfTarget, resolveLaneModel, FOREGROUND_ONLY_RULE, STANDING_AUTHORIZATION, spawnLaneWorker, spawnEnvelopeRepairRequest, laneRunDir, readSpawnManifest, stopLaneProcesses, } from "../lib/lanes/spawn.js";
|
|
87
89
|
import { handOffInstallHere } from "../lib/daemon/handoff.js";
|
|
88
90
|
export { spawnLaneWorker };
|
|
91
|
+
export function openPullRequestsForProject(hqRoot, metadata) {
|
|
92
|
+
const configured = metadata.repos;
|
|
93
|
+
if (!Array.isArray(configured)) {
|
|
94
|
+
throw new Error("project does not name repositories");
|
|
95
|
+
}
|
|
96
|
+
if (configured.length === 0)
|
|
97
|
+
return [];
|
|
98
|
+
const all = [];
|
|
99
|
+
const reposRoot = fs.realpathSync(path.resolve(hqRoot, "repos")) + path.sep;
|
|
100
|
+
for (const repo of configured) {
|
|
101
|
+
if (repo === null || typeof repo !== "object" || typeof repo.path !== "string") {
|
|
102
|
+
throw new Error("project repository entry is invalid");
|
|
103
|
+
}
|
|
104
|
+
const repoPath = repo.path;
|
|
105
|
+
if (!/^repos\/(private|public)\/[a-zA-Z0-9._/-]+$/.test(repoPath)) {
|
|
106
|
+
throw new Error("project repository path is outside repos");
|
|
107
|
+
}
|
|
108
|
+
if (repoPath.split("/").some((segment) => segment === "." || segment === "..")) {
|
|
109
|
+
throw new Error("project repository path is outside repos");
|
|
110
|
+
}
|
|
111
|
+
const repoDir = path.resolve(hqRoot, repoPath);
|
|
112
|
+
const canonicalRepoDir = fs.realpathSync(repoDir);
|
|
113
|
+
if (!canonicalRepoDir.startsWith(reposRoot))
|
|
114
|
+
throw new Error("project repository path is outside repos");
|
|
115
|
+
const remoteResult = spawnSync("git", ["-C", repoDir, "remote", "get-url", "origin"], {
|
|
116
|
+
encoding: "utf8",
|
|
117
|
+
timeout: 5_000,
|
|
118
|
+
windowsHide: true,
|
|
119
|
+
});
|
|
120
|
+
if (remoteResult.status !== 0)
|
|
121
|
+
throw new Error("project repository remote could not be read");
|
|
122
|
+
const remote = remoteResult.stdout.trim();
|
|
123
|
+
const match = remote.match(/^(?:https:\/\/github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?\/?$/i);
|
|
124
|
+
if (!match)
|
|
125
|
+
throw new Error("project repository is not a GitHub repository");
|
|
126
|
+
const endpoint = `repos/${match[1]}/${match[2]}/pulls?state=open&per_page=100`;
|
|
127
|
+
const result = spawnSync("gh", ["api", "--paginate", "--slurp", endpoint], {
|
|
128
|
+
encoding: "utf8",
|
|
129
|
+
timeout: 20_000,
|
|
130
|
+
windowsHide: true,
|
|
131
|
+
});
|
|
132
|
+
if (result.status !== 0)
|
|
133
|
+
throw new Error("GitHub pull request lookup failed");
|
|
134
|
+
const pages = JSON.parse(result.stdout || "[]");
|
|
135
|
+
if (!Array.isArray(pages))
|
|
136
|
+
throw new Error("GitHub pull request response was invalid");
|
|
137
|
+
for (const page of pages) {
|
|
138
|
+
if (!Array.isArray(page))
|
|
139
|
+
throw new Error("GitHub pull request page was invalid");
|
|
140
|
+
for (const item of page) {
|
|
141
|
+
if (item === null || typeof item !== "object")
|
|
142
|
+
throw new Error("GitHub pull request entry was invalid");
|
|
143
|
+
const pr = item;
|
|
144
|
+
if (!Number.isInteger(pr.number) || typeof pr.created_at !== "string" || !Number.isFinite(Date.parse(pr.created_at))) {
|
|
145
|
+
throw new Error("GitHub pull request entry was invalid");
|
|
146
|
+
}
|
|
147
|
+
all.push({ number: pr.number, created_at: pr.created_at, repository: `${match[1]}/${match[2]}` });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return all;
|
|
152
|
+
}
|
|
153
|
+
async function focusGuardAssessment(ctx, input) {
|
|
154
|
+
if (input.overrideReason !== undefined) {
|
|
155
|
+
const normalizedReason = input.overrideReason.trim();
|
|
156
|
+
if (!normalizedReason) {
|
|
157
|
+
throw new LanesError("focus_override_reason_required", "--focus-override requires a non-empty reason", {
|
|
158
|
+
story_ids: input.storyIds,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
assertNoSecretText("focus override reason", normalizedReason);
|
|
162
|
+
}
|
|
163
|
+
let enabled = false;
|
|
164
|
+
try {
|
|
165
|
+
enabled = ctx.focusGuardEnabled
|
|
166
|
+
? await ctx.focusGuardEnabled(input.company ?? undefined)
|
|
167
|
+
: await readLanesRegistryFlagFailOff(LANE_FOCUS_GUARD_FLAG, {
|
|
168
|
+
company: input.company,
|
|
169
|
+
env: ctx.env,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
process.stderr.write(`${JSON.stringify({
|
|
174
|
+
event: "lanes.focus_guard.flag_read_failed",
|
|
175
|
+
error: error instanceof Error ? error.name : "UnknownError",
|
|
176
|
+
fallback: false,
|
|
177
|
+
})}\n`);
|
|
178
|
+
}
|
|
179
|
+
if (!enabled)
|
|
180
|
+
return { enabled: false, reasons: [] };
|
|
181
|
+
const project = readProjectStoryFocusData(ctx.hqRoot(), input.projectId, input.company?.slug);
|
|
182
|
+
let pullRequests;
|
|
183
|
+
let pullRequestLookupFailed = false;
|
|
184
|
+
if (input.checkTeamBlockers) {
|
|
185
|
+
try {
|
|
186
|
+
pullRequests = openPullRequestsForProject(ctx.hqRoot(), project.metadata);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
pullRequestLookupFailed = true;
|
|
190
|
+
process.stderr.write(`${JSON.stringify({
|
|
191
|
+
event: "lanes.focus_guard.pr_lookup_failed",
|
|
192
|
+
project_id: input.projectId,
|
|
193
|
+
error: error instanceof Error ? error.name : "UnknownError",
|
|
194
|
+
})}\n`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const tenantLanes = loadAllLanes(ctx.hqRoot()).filter((lane) => lane.company?.slug === input.company?.slug && (lane.project_id === input.projectId ||
|
|
198
|
+
lane.stories?.some((story) => story.project_id === input.projectId) === true));
|
|
199
|
+
const reasons = assertFocusGuardAllows(enabled, {
|
|
200
|
+
storyIds: input.storyIds,
|
|
201
|
+
stories: project.userStories.map((story) => ({
|
|
202
|
+
id: typeof story.id === "string" ? story.id : "",
|
|
203
|
+
focus_id: story.focus_id,
|
|
204
|
+
kpi: story.kpi,
|
|
205
|
+
state: story.state,
|
|
206
|
+
status: story.status,
|
|
207
|
+
lane: story.lane,
|
|
208
|
+
lanes: story.lanes,
|
|
209
|
+
})),
|
|
210
|
+
lanes: tenantLanes,
|
|
211
|
+
...(pullRequests === undefined ? {} : { pullRequests }),
|
|
212
|
+
pullRequestLookupFailed,
|
|
213
|
+
now: ctx.now(),
|
|
214
|
+
checkTeamBlockers: input.checkTeamBlockers,
|
|
215
|
+
}, input.overrideReason);
|
|
216
|
+
return { enabled: true, reasons };
|
|
217
|
+
}
|
|
89
218
|
export { questionBodyAbsPath };
|
|
90
219
|
export { MAX_LANE_DEPTH };
|
|
91
220
|
export const LANES_RUNS_REL = path.join("workspace", "lanes-runs");
|
|
@@ -329,6 +458,7 @@ function resolveDeps(deps = {}) {
|
|
|
329
458
|
fetchText: deps.fetchText ?? defaultFetchText,
|
|
330
459
|
lanePlacementEnabled: deps.lanePlacementEnabled ?? defaultLanePlacementEnabled,
|
|
331
460
|
storyLedgerEnabled: deps.storyLedgerEnabled ?? laneStoryLedgerEnabled,
|
|
461
|
+
focusGuardEnabled: deps.focusGuardEnabled,
|
|
332
462
|
isSsmOwnerDispatch: deps.isSsmOwnerDispatch ?? (() => isSsmRunCommandProcess()),
|
|
333
463
|
withGrantHireLocks: deps.withGrantHireLocks ?? withLaneHireLocks,
|
|
334
464
|
mergeReviewEnabled: deps.mergeReviewEnabled,
|
|
@@ -3033,6 +3163,12 @@ async function runCreate(ctx, opts) {
|
|
|
3033
3163
|
if (opts.story !== undefined && opts.noStory !== undefined) {
|
|
3034
3164
|
throw new LanesError("story_selection_conflict", "Pass either --story or --no-story, not both", {});
|
|
3035
3165
|
}
|
|
3166
|
+
if (opts.focusOverride !== undefined) {
|
|
3167
|
+
if (!opts.focusOverride.trim()) {
|
|
3168
|
+
throw new LanesError("focus_override_reason_required", "--focus-override requires a non-empty reason", {});
|
|
3169
|
+
}
|
|
3170
|
+
assertNoSecretText("focus override reason", opts.focusOverride.trim());
|
|
3171
|
+
}
|
|
3036
3172
|
if (opts.noStory !== undefined && !noStoryReason) {
|
|
3037
3173
|
throw new LanesError("missing_no_story_reason", "--no-story requires a non-empty reason", {});
|
|
3038
3174
|
}
|
|
@@ -3046,6 +3182,9 @@ async function runCreate(ctx, opts) {
|
|
|
3046
3182
|
throw new LanesError("missing_story", "--additional-story values must be non-empty", {});
|
|
3047
3183
|
}
|
|
3048
3184
|
const storyIds = story ? [story, ...additionalStories] : [];
|
|
3185
|
+
if (opts.focusOverride !== undefined && storyIds.length === 0) {
|
|
3186
|
+
throw new LanesError("focus_override_story_required", "--focus-override requires --story", {});
|
|
3187
|
+
}
|
|
3049
3188
|
if (new Set(storyIds).size !== storyIds.length) {
|
|
3050
3189
|
throw new LanesError("duplicate_story", "A lane cannot carry the same project/story more than once", {
|
|
3051
3190
|
project_id: project,
|
|
@@ -3113,6 +3252,15 @@ async function runCreate(ctx, opts) {
|
|
|
3113
3252
|
if (storyIds.length > 0 && ctx.storyLedgerEnabled()) {
|
|
3114
3253
|
validateProjectStories(hqRoot, project, storyIds, company?.slug);
|
|
3115
3254
|
}
|
|
3255
|
+
const focusAssessment = storyIds.length > 0
|
|
3256
|
+
? await focusGuardAssessment(ctx, {
|
|
3257
|
+
projectId: project,
|
|
3258
|
+
storyIds,
|
|
3259
|
+
company,
|
|
3260
|
+
checkTeamBlockers: true,
|
|
3261
|
+
overrideReason: opts.focusOverride,
|
|
3262
|
+
})
|
|
3263
|
+
: { enabled: false, reasons: [] };
|
|
3116
3264
|
let meshCompany = company;
|
|
3117
3265
|
if (meshCompanyFlag) {
|
|
3118
3266
|
const resolvedMeshCompany = await ctx.resolveCompany(meshCompanyFlag);
|
|
@@ -3767,6 +3915,53 @@ async function runCreate(ctx, opts) {
|
|
|
3767
3915
|
}
|
|
3768
3916
|
let lane = persisted.lane;
|
|
3769
3917
|
const { admission, validation } = persisted;
|
|
3918
|
+
if (focusAssessment.enabled && opts.focusOverride !== undefined) {
|
|
3919
|
+
const focusOverride = makeFocusOverride({
|
|
3920
|
+
reason: opts.focusOverride,
|
|
3921
|
+
laneId: lane.lane_id,
|
|
3922
|
+
at: ctx.now().toISOString(),
|
|
3923
|
+
storyId: story ?? "",
|
|
3924
|
+
});
|
|
3925
|
+
try {
|
|
3926
|
+
appendProjectFocusOverride(hqRoot, project, company?.slug, focusOverride);
|
|
3927
|
+
lane = casUpdateLane(hqRoot, lane.lane_id, (current) => ({
|
|
3928
|
+
...appendLaneFocusOverride(current, focusOverride),
|
|
3929
|
+
}));
|
|
3930
|
+
}
|
|
3931
|
+
catch (error) {
|
|
3932
|
+
try {
|
|
3933
|
+
removeProjectFocusOverride(hqRoot, project, company?.slug, focusOverride);
|
|
3934
|
+
}
|
|
3935
|
+
catch (cleanupError) {
|
|
3936
|
+
process.stderr.write(`${JSON.stringify({
|
|
3937
|
+
event: "lanes.focus_guard.override_rollback_failed",
|
|
3938
|
+
lane_id: lane.lane_id,
|
|
3939
|
+
error: cleanupError instanceof Error ? cleanupError.name : "UnknownError",
|
|
3940
|
+
})}\n`);
|
|
3941
|
+
}
|
|
3942
|
+
try {
|
|
3943
|
+
casUpdateLane(hqRoot, lane.lane_id, (current) => ({
|
|
3944
|
+
...current,
|
|
3945
|
+
state: "failed",
|
|
3946
|
+
queued_reason: undefined,
|
|
3947
|
+
queued_details: undefined,
|
|
3948
|
+
admission_reserved_at: undefined,
|
|
3949
|
+
terminal_reason: "focus_override_persist_failed",
|
|
3950
|
+
}));
|
|
3951
|
+
}
|
|
3952
|
+
catch (markError) {
|
|
3953
|
+
process.stderr.write(`${JSON.stringify({
|
|
3954
|
+
event: "lanes.focus_guard.lane_fail_mark_failed",
|
|
3955
|
+
lane_id: lane.lane_id,
|
|
3956
|
+
error: markError instanceof Error ? markError.name : "UnknownError",
|
|
3957
|
+
})}\n`);
|
|
3958
|
+
}
|
|
3959
|
+
throw new LanesError("focus_override_persist_failed", "Could not persist the focus override; lane launch was refused", {
|
|
3960
|
+
lane_id: lane.lane_id,
|
|
3961
|
+
error: error instanceof Error ? error.name : "UnknownError",
|
|
3962
|
+
});
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3770
3965
|
if (meshCompany) {
|
|
3771
3966
|
try {
|
|
3772
3967
|
lane = await ensureLaneThreadRecord(ctx, lane);
|
|
@@ -4521,6 +4716,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
|
|
|
4521
4716
|
text: options.text,
|
|
4522
4717
|
project: options.project,
|
|
4523
4718
|
story: options.story,
|
|
4719
|
+
focusOverride: options.focusOverride,
|
|
4524
4720
|
account: options.account,
|
|
4525
4721
|
model: options.model,
|
|
4526
4722
|
effort: options.effort,
|
|
@@ -5939,17 +6135,23 @@ async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContex
|
|
|
5939
6135
|
assigned_child_grants: allocated,
|
|
5940
6136
|
});
|
|
5941
6137
|
}
|
|
5942
|
-
return retryUpdateLane(hqRoot, laneId, "command", (record) => (
|
|
5943
|
-
...record,
|
|
5944
|
-
hire_grant: hireGrant
|
|
5945
|
-
}));
|
|
6138
|
+
return retryUpdateLane(hqRoot, laneId, "command", (record) => isTerminalLaneState(record.state)
|
|
6139
|
+
? { ...record, hire_grant: 0, released_hire_grant: hireGrant }
|
|
6140
|
+
: { ...record, hire_grant: hireGrant });
|
|
5946
6141
|
});
|
|
5947
6142
|
return updated;
|
|
5948
6143
|
}
|
|
5949
6144
|
async function printGrantResult(ctx, hqRoot, updated, hierarchy) {
|
|
5950
6145
|
const finishedChildrenDoNotCount = await hireGrantFinishedChildrenDoNotCount(ctx, updated);
|
|
5951
6146
|
const fields = laneHireFields(updated, hierarchy ?? loadAllLanes(hqRoot), undefined, finishedChildrenDoNotCount);
|
|
5952
|
-
printJson({
|
|
6147
|
+
printJson({
|
|
6148
|
+
ok: true,
|
|
6149
|
+
action: "grant",
|
|
6150
|
+
lane_id: updated.lane_id,
|
|
6151
|
+
lane: updated,
|
|
6152
|
+
...fields,
|
|
6153
|
+
hire_grant: updated.released_hire_grant ?? fields.hire_grant,
|
|
6154
|
+
});
|
|
5953
6155
|
}
|
|
5954
6156
|
/**
|
|
5955
6157
|
* `hq lanes set`: change the model or effort a lane uses from its next
|
|
@@ -5958,8 +6160,15 @@ async function printGrantResult(ctx, hqRoot, updated, hierarchy) {
|
|
|
5958
6160
|
* next `set` or an explicit resume `--effort`/`--worker`.
|
|
5959
6161
|
*/
|
|
5960
6162
|
async function runSet(ctx, laneId, opts) {
|
|
6163
|
+
if (opts.company !== undefined) {
|
|
6164
|
+
if (opts.model !== undefined || opts.effort !== undefined || opts.reason !== undefined) {
|
|
6165
|
+
throw new LanesError("lane_set_company_exclusive", "--company cannot be combined with --model, --effort, or --reason", { lane_id: laneId });
|
|
6166
|
+
}
|
|
6167
|
+
await runRepairCompany(ctx, laneId, { company: opts.company });
|
|
6168
|
+
return;
|
|
6169
|
+
}
|
|
5961
6170
|
if (opts.model === undefined && opts.effort === undefined) {
|
|
5962
|
-
throw new LanesError("lane_set_empty", "hq lanes set needs --model or --effort", { lane_id: laneId });
|
|
6171
|
+
throw new LanesError("lane_set_empty", "hq lanes set needs --company, --model, or --effort", { lane_id: laneId });
|
|
5963
6172
|
}
|
|
5964
6173
|
const reason = opts.reason?.trim();
|
|
5965
6174
|
if (opts.reason !== undefined && !reason) {
|
|
@@ -7573,7 +7782,7 @@ async function repairCompanyOnOwner(ctx, laneId, opts, emitResult = true) {
|
|
|
7573
7782
|
if (current.company !== null) {
|
|
7574
7783
|
if (sameLaneCompany(current.company, company))
|
|
7575
7784
|
return current;
|
|
7576
|
-
throw new LanesError("lane_company_already_set", `
|
|
7785
|
+
throw new LanesError("lane_company_already_set", `Cannot set lane ${laneId} to ${company.slug}: it already belongs to ${current.company.slug}`, { lane_id: laneId, company: current.company.slug, requested_company: company.slug });
|
|
7577
7786
|
}
|
|
7578
7787
|
return {
|
|
7579
7788
|
...current,
|
|
@@ -8879,6 +9088,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8879
9088
|
casUpdateLane,
|
|
8880
9089
|
dispatchOutpostLaneCommand,
|
|
8881
9090
|
dispatchPrimaryLaneMessage,
|
|
9091
|
+
focusGuardAssessment,
|
|
8882
9092
|
drainHookStatusFor,
|
|
8883
9093
|
laneIsHostedHere,
|
|
8884
9094
|
laneWithRecoveredProviderSession,
|
|
@@ -8910,6 +9120,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8910
9120
|
automaticBalanceMoveHistoryReason,
|
|
8911
9121
|
authorizeFailedResume,
|
|
8912
9122
|
authorizeResumeProfileRepin,
|
|
9123
|
+
focusGuardAssessment,
|
|
8913
9124
|
casUpdateLane,
|
|
8914
9125
|
dispatchOutpostLaneCommand: async (commandCtx, lane, action, options, behavior) => {
|
|
8915
9126
|
await dispatchOutpostLaneCommand(commandCtx, lane, action, options, behavior);
|
|
@@ -8985,6 +9196,10 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8985
9196
|
hqRoot: ctx.hqRoot,
|
|
8986
9197
|
resolveCompany: ctx.resolveCompany,
|
|
8987
9198
|
now: ctx.now,
|
|
9199
|
+
focusDashboardEnabled: (company) => readLanesRegistryFlagFailOff(LANE_FOCUS_DASHBOARD_FLAG, {
|
|
9200
|
+
company,
|
|
9201
|
+
env: ctx.env,
|
|
9202
|
+
}, readLanesRegistryFlag),
|
|
8988
9203
|
readQuestionBody: (root, question) => {
|
|
8989
9204
|
const body = questionHandlers.requireQuestionBody(root, question);
|
|
8990
9205
|
assertQuestionBodySecretSafe(body);
|
|
@@ -9021,6 +9236,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9021
9236
|
.option("--mesh-company <slug|uid>", "Explicit Work Mesh company for a personal lane; membership is verified")
|
|
9022
9237
|
.requiredOption("--project <slug>", "HQ project slug")
|
|
9023
9238
|
.option("--story <id>", "Story id from the project's prd.json")
|
|
9239
|
+
.option("--focus-override <reason>", "Human-approved reason to override the lane focus guard")
|
|
9024
9240
|
.addOption(noStoryOption)
|
|
9025
9241
|
.option("--additional-story <id>", "Additional story carried by this lane; repeatable and discouraged when active", collectRepeatable)
|
|
9026
9242
|
.requiredOption("--brief-file <path>", "Path to the lane brief")
|
|
@@ -9083,8 +9299,9 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9083
9299
|
}));
|
|
9084
9300
|
lanes
|
|
9085
9301
|
.command("set")
|
|
9086
|
-
.description("Change
|
|
9302
|
+
.description("Change lane settings or bind its missing company attribution")
|
|
9087
9303
|
.argument("<lane-id>", "Lane id")
|
|
9304
|
+
.option("--company <slug>", "Bind missing company attribution to this lane")
|
|
9088
9305
|
.option("--model <model>", "Model for the lane's next and later turns")
|
|
9089
9306
|
.option("--effort <level>", "Effort for the lane's next and later turns")
|
|
9090
9307
|
.option("--reason <text>", "Why the settings changed; recorded in settings_history")
|
|
@@ -9295,6 +9512,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9295
9512
|
.option("--text-file <path>", "Read message text from a file")
|
|
9296
9513
|
.option("--project <slug>", "Re-point project attribution; a changed story is named in the resumed prompt")
|
|
9297
9514
|
.option("--story <id>", "Re-point story attribution; a changed story is named in the resumed prompt")
|
|
9515
|
+
.option("--focus-override <reason>", "Human-approved reason to override the lane focus guard")
|
|
9298
9516
|
.option("--account <id>", "Move the lane's session to this account of its provider before the message resumes it")
|
|
9299
9517
|
.option("--model <id>", "Model for the moved lane (with --account only)")
|
|
9300
9518
|
.option("--effort <level>", "Reasoning effort for the resumed provider turn")
|
|
@@ -9351,6 +9569,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9351
9569
|
.option("--reason <text>", "Required explanation for resuming from failed")
|
|
9352
9570
|
.option("--project <slug>", "Re-point project attribution; a changed story is named in the resumed prompt")
|
|
9353
9571
|
.option("--story <id>", "Re-point story attribution; a changed story is named in the resumed prompt")
|
|
9572
|
+
.option("--focus-override <reason>", "Human-approved reason to override the lane focus guard")
|
|
9354
9573
|
.option("--account <id>", "Move the lane's session to this account of its provider before resuming")
|
|
9355
9574
|
.option("--model <id>", "Model for the moved lane (with --account only)")
|
|
9356
9575
|
.option("--effort <level>", "Reasoning effort for the resumed provider turn")
|
|
@@ -520,7 +520,10 @@ export function registerMeetingsCommand(program) {
|
|
|
520
520
|
}
|
|
521
521
|
const token = await ensureCognitoToken();
|
|
522
522
|
const companyId = await getCompanyUid(token, companySlug);
|
|
523
|
-
|
|
523
|
+
// The import endpoint records a person as the recorder (prs_* only).
|
|
524
|
+
const recorderPersonUid = await resolveCallerPersonUid(token, undefined, {
|
|
525
|
+
command: "hq meetings import",
|
|
526
|
+
});
|
|
524
527
|
const res = await vaultApiFetch({
|
|
525
528
|
token,
|
|
526
529
|
method: "POST",
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { CLI_COMMAND_HELP_CATALOG_FLAG } from "./lib/cli-flag-keys.js";
|
|
|
12
12
|
// heavy import (main.js) is lazy and reached only when a rejection fires.
|
|
13
13
|
import { installProcessRejectionBoundary } from "./unhandled-rejection-boundary.js";
|
|
14
14
|
import { installFastPathStreamErrorHandlers } from "./utils/fast-stream-errors.js";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
15
16
|
// Install BEFORE any dispatch so a floated rejection on ANY path (the command
|
|
16
17
|
// graph or the fast-core forwarder) becomes a non-zero exit. @sentry/node's
|
|
17
18
|
// onUnhandledRejection integration defaults to 'warn' — it captures but does
|
|
@@ -20,6 +21,44 @@ import { installFastPathStreamErrorHandlers } from "./utils/fast-stream-errors.j
|
|
|
20
21
|
// so runCli's finally still finalizes release health and flushes Sentry.
|
|
21
22
|
installProcessRejectionBoundary();
|
|
22
23
|
installFastPathStreamErrorHandlers();
|
|
24
|
+
async function runMainWithInstallTreeRecovery() {
|
|
25
|
+
let runCli;
|
|
26
|
+
const { registerCommandsWithRecovery } = await import("./startup-registration.js");
|
|
27
|
+
try {
|
|
28
|
+
const result = await registerCommandsWithRecovery({
|
|
29
|
+
register: async () => {
|
|
30
|
+
runCli = (await import("./main.js")).runCli;
|
|
31
|
+
},
|
|
32
|
+
argv: process.argv,
|
|
33
|
+
env: process.env,
|
|
34
|
+
entryPath: fileURLToPath(import.meta.url),
|
|
35
|
+
stderr: process.stderr,
|
|
36
|
+
});
|
|
37
|
+
if (result.reexecStatus !== undefined) {
|
|
38
|
+
process.exitCode = result.reexecStatus;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (!runCli)
|
|
42
|
+
throw new Error("HQ CLI startup did not load its command runner");
|
|
43
|
+
await runCli();
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
// main.js may be the module that is missing, so the global rejection
|
|
47
|
+
// reporter cannot import its usual handler to print the recovery remedy.
|
|
48
|
+
const installTree = await import("./utils/install-tree-torn.js");
|
|
49
|
+
if (installTree.isInstallTreeTornError(error)) {
|
|
50
|
+
process.stderr.write(`${installTree.installTreeTornStderrLine(error)}\n`);
|
|
51
|
+
const sentry = await import("./sentry.js");
|
|
52
|
+
sentry.initSentry();
|
|
53
|
+
sentry.Sentry.captureException(error, installTree.installTreeTornCaptureContext(error));
|
|
54
|
+
sentry.finishSentrySession();
|
|
55
|
+
await sentry.Sentry.flush(2_000);
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
23
62
|
function dispatchCliEntry() {
|
|
24
63
|
const versionRequest = isVersionRequest(process.argv);
|
|
25
64
|
const helpRequest = !versionRequest && isCommandHelpRequest(process.argv);
|
|
@@ -51,8 +90,7 @@ function dispatchCliEntry() {
|
|
|
51
90
|
await runCommandHelp(process.argv);
|
|
52
91
|
return;
|
|
53
92
|
}
|
|
54
|
-
|
|
55
|
-
await runCli();
|
|
93
|
+
await runMainWithInstallTreeRecovery();
|
|
56
94
|
});
|
|
57
95
|
}
|
|
58
96
|
else if (parsedFastCore?.entry.kind === "native" &&
|
|
@@ -73,8 +111,7 @@ function dispatchCliEntry() {
|
|
|
73
111
|
await runFastCore(process.argv, parsedFastCore);
|
|
74
112
|
}
|
|
75
113
|
else {
|
|
76
|
-
|
|
77
|
-
await runCli();
|
|
114
|
+
await runMainWithInstallTreeRecovery();
|
|
78
115
|
}
|
|
79
116
|
});
|
|
80
117
|
}
|
|
@@ -88,8 +125,7 @@ function dispatchCliEntry() {
|
|
|
88
125
|
return runSecretsEnvFastPath(request);
|
|
89
126
|
},
|
|
90
127
|
runCurrent: async () => {
|
|
91
|
-
|
|
92
|
-
await runCli();
|
|
128
|
+
await runMainWithInstallTreeRecovery();
|
|
93
129
|
},
|
|
94
130
|
});
|
|
95
131
|
});
|
|
@@ -111,7 +147,7 @@ function dispatchCliEntry() {
|
|
|
111
147
|
// successful command into exit 13 when that teardown promise remains pending.
|
|
112
148
|
// A floated rejection here no longer exits 0 silently: the boundary installed
|
|
113
149
|
// above turns it into a non-zero exit (HQ-CLI-W).
|
|
114
|
-
void
|
|
150
|
+
void runMainWithInstallTreeRecovery();
|
|
115
151
|
}
|
|
116
152
|
}
|
|
117
153
|
function isNpmPackageEntry(entryUrl) {
|
|
@@ -147,8 +147,11 @@ const SAFE_SLOW_PHASES = new Set([
|
|
|
147
147
|
"child_wait",
|
|
148
148
|
"probe",
|
|
149
149
|
"parse",
|
|
150
|
+
"no_phase_recorded",
|
|
150
151
|
]);
|
|
151
|
-
|
|
152
|
+
// This sentinel describes an absent active-phase record, not a phase a hook
|
|
153
|
+
// measures in phase_timings.
|
|
154
|
+
const DEBUG_CONTEXT_PHASES = new Set([...SAFE_SLOW_PHASES].filter((phase) => phase !== "no_phase_recorded"));
|
|
152
155
|
/**
|
|
153
156
|
* The only hook diagnostics that can be attached as metadata. The list omits
|
|
154
157
|
* command text, prompts, payloads, company names, and repositories. `hook_path`
|
|
@@ -278,6 +281,10 @@ function safeSlowPhase(value) {
|
|
|
278
281
|
}
|
|
279
282
|
function resolvedSlowPhase(metadata) {
|
|
280
283
|
const reported = safeSlowPhase(metadata.slow_phase);
|
|
284
|
+
// The hook explicitly recorded that no phase was active. Do not replace that
|
|
285
|
+
// finding with a different phase inferred from aggregate timing data.
|
|
286
|
+
if (reported === "no_phase_recorded")
|
|
287
|
+
return reported;
|
|
281
288
|
if (reported !== "unknown" && reported !== "other")
|
|
282
289
|
return reported;
|
|
283
290
|
// Which measured master-hook phase consumed the most time when the caller's
|
|
@@ -130,4 +130,9 @@ export declare class ControlPlaneDbClient {
|
|
|
130
130
|
appId: string;
|
|
131
131
|
}): Promise<RemoteDestroyAppResponse>;
|
|
132
132
|
}
|
|
133
|
+
export type ControlPlaneHttpError = Error & {
|
|
134
|
+
status?: number;
|
|
135
|
+
code?: string;
|
|
136
|
+
reason?: string;
|
|
137
|
+
};
|
|
133
138
|
//# sourceMappingURL=control-plane.d.ts.map
|
|
@@ -48,6 +48,8 @@ export class ControlPlaneDbClient {
|
|
|
48
48
|
err.status = res.status;
|
|
49
49
|
if (typeof payload?.code === "string")
|
|
50
50
|
err.code = payload.code;
|
|
51
|
+
if (typeof payload?.reason === "string")
|
|
52
|
+
err.reason = payload.reason;
|
|
51
53
|
throw err;
|
|
52
54
|
}
|
|
53
55
|
return JSON.parse(text);
|
|
@@ -145,12 +147,15 @@ export class ControlPlaneDbClient {
|
|
|
145
147
|
function httpError(label, status, text) {
|
|
146
148
|
let msg = `${label} failed (${status})`;
|
|
147
149
|
let code;
|
|
150
|
+
let reason;
|
|
148
151
|
try {
|
|
149
152
|
const j = JSON.parse(text);
|
|
150
153
|
if (typeof j.error === "string")
|
|
151
154
|
msg = j.error;
|
|
152
155
|
if (typeof j.code === "string")
|
|
153
156
|
code = j.code;
|
|
157
|
+
if (typeof j.reason === "string")
|
|
158
|
+
reason = j.reason;
|
|
154
159
|
}
|
|
155
160
|
catch {
|
|
156
161
|
/* keep */
|
|
@@ -159,6 +164,8 @@ function httpError(label, status, text) {
|
|
|
159
164
|
err.status = status;
|
|
160
165
|
if (code)
|
|
161
166
|
err.code = code;
|
|
167
|
+
if (reason)
|
|
168
|
+
err.reason = reason;
|
|
162
169
|
return err;
|
|
163
170
|
}
|
|
164
171
|
//# sourceMappingURL=control-plane.js.map
|