@indigoai-us/hq-cli 5.345.44 → 5.345.46
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 +17 -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/install-global.d.ts +3 -0
- package/dist/commands/install-global.js +24 -2
- 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 +253 -15
- package/dist/commands/meetings.js +4 -1
- package/dist/lib/anywhere-person-setting.d.ts +11 -0
- package/dist/lib/anywhere-person-setting.js +19 -0
- package/dist/lib/daemon/ops/mcp.d.ts +10 -1
- package/dist/lib/daemon/ops/mcp.js +17 -10
- package/dist/lib/daemon/run.js +8 -2
- package/dist/lib/doctor/checks/sync-runtime.d.ts +2 -0
- package/dist/lib/doctor/checks/sync-runtime.js +26 -12
- package/dist/lib/flag-snapshot-file.js +33 -25
- package/dist/lib/install/claude-targets.js +7 -2
- package/dist/lib/install/codex-targets.d.ts +6 -0
- package/dist/lib/install/codex-targets.js +25 -0
- 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/hook-trust.d.ts +7 -0
- package/dist/utils/hook-trust.js +40 -14
- 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/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,
|
|
@@ -1726,11 +1856,13 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
|
|
|
1726
1856
|
const companyMatches = !hostlessUlidLookup || ownerLookupGate === true || (ownerLookupGate === "check-company" &&
|
|
1727
1857
|
laneCompany !== undefined && laneCompany !== null &&
|
|
1728
1858
|
await options.allowHostlessOwnerLookup(laneCompany));
|
|
1729
|
-
|
|
1859
|
+
const exactOutpostOwnedMatch = !hostlessUlidLookup &&
|
|
1860
|
+
lane.lane_id === laneId && lane.host.kind === "outpost" && lane.host.id === outpostId;
|
|
1861
|
+
if (exactOutpostOwnedMatch || (lane.host.kind === "local" && ((!hostlessUlidLookup && lane.lane_id === laneId &&
|
|
1730
1862
|
sanitizeHostComponent(lane.host.id) === hostSuffix) ||
|
|
1731
1863
|
(hostlessUlidLookup && laneUlidPart(lane.lane_id) === requestedUlid &&
|
|
1732
1864
|
sanitizeHostComponent(lane.host.id) === recordedSuffix &&
|
|
1733
|
-
companyMatches))) {
|
|
1865
|
+
companyMatches)))) {
|
|
1734
1866
|
matches.push({ outpostId, lane });
|
|
1735
1867
|
}
|
|
1736
1868
|
}
|
|
@@ -1784,7 +1916,7 @@ function commandLaneNotFound(laneId, outpostHint, details = {}) {
|
|
|
1784
1916
|
const hint = outpostHint
|
|
1785
1917
|
? "; pass --outpost <id> to confirm a registered Outpost"
|
|
1786
1918
|
: "";
|
|
1787
|
-
return new LanesError("lane_not_found", `
|
|
1919
|
+
return new LanesError("lane_not_found", `Could not resolve lane ${laneId}: no matching local record or reachable registered Outpost owner record was found${hint}`, { lane_id: laneId, ...details });
|
|
1788
1920
|
}
|
|
1789
1921
|
async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config, options = {}) {
|
|
1790
1922
|
const localHost = laneHostFromCtx(ctx);
|
|
@@ -3033,6 +3165,12 @@ async function runCreate(ctx, opts) {
|
|
|
3033
3165
|
if (opts.story !== undefined && opts.noStory !== undefined) {
|
|
3034
3166
|
throw new LanesError("story_selection_conflict", "Pass either --story or --no-story, not both", {});
|
|
3035
3167
|
}
|
|
3168
|
+
if (opts.focusOverride !== undefined) {
|
|
3169
|
+
if (!opts.focusOverride.trim()) {
|
|
3170
|
+
throw new LanesError("focus_override_reason_required", "--focus-override requires a non-empty reason", {});
|
|
3171
|
+
}
|
|
3172
|
+
assertNoSecretText("focus override reason", opts.focusOverride.trim());
|
|
3173
|
+
}
|
|
3036
3174
|
if (opts.noStory !== undefined && !noStoryReason) {
|
|
3037
3175
|
throw new LanesError("missing_no_story_reason", "--no-story requires a non-empty reason", {});
|
|
3038
3176
|
}
|
|
@@ -3046,6 +3184,9 @@ async function runCreate(ctx, opts) {
|
|
|
3046
3184
|
throw new LanesError("missing_story", "--additional-story values must be non-empty", {});
|
|
3047
3185
|
}
|
|
3048
3186
|
const storyIds = story ? [story, ...additionalStories] : [];
|
|
3187
|
+
if (opts.focusOverride !== undefined && storyIds.length === 0) {
|
|
3188
|
+
throw new LanesError("focus_override_story_required", "--focus-override requires --story", {});
|
|
3189
|
+
}
|
|
3049
3190
|
if (new Set(storyIds).size !== storyIds.length) {
|
|
3050
3191
|
throw new LanesError("duplicate_story", "A lane cannot carry the same project/story more than once", {
|
|
3051
3192
|
project_id: project,
|
|
@@ -3113,6 +3254,15 @@ async function runCreate(ctx, opts) {
|
|
|
3113
3254
|
if (storyIds.length > 0 && ctx.storyLedgerEnabled()) {
|
|
3114
3255
|
validateProjectStories(hqRoot, project, storyIds, company?.slug);
|
|
3115
3256
|
}
|
|
3257
|
+
const focusAssessment = storyIds.length > 0
|
|
3258
|
+
? await focusGuardAssessment(ctx, {
|
|
3259
|
+
projectId: project,
|
|
3260
|
+
storyIds,
|
|
3261
|
+
company,
|
|
3262
|
+
checkTeamBlockers: true,
|
|
3263
|
+
overrideReason: opts.focusOverride,
|
|
3264
|
+
})
|
|
3265
|
+
: { enabled: false, reasons: [] };
|
|
3116
3266
|
let meshCompany = company;
|
|
3117
3267
|
if (meshCompanyFlag) {
|
|
3118
3268
|
const resolvedMeshCompany = await ctx.resolveCompany(meshCompanyFlag);
|
|
@@ -3767,6 +3917,53 @@ async function runCreate(ctx, opts) {
|
|
|
3767
3917
|
}
|
|
3768
3918
|
let lane = persisted.lane;
|
|
3769
3919
|
const { admission, validation } = persisted;
|
|
3920
|
+
if (focusAssessment.enabled && opts.focusOverride !== undefined) {
|
|
3921
|
+
const focusOverride = makeFocusOverride({
|
|
3922
|
+
reason: opts.focusOverride,
|
|
3923
|
+
laneId: lane.lane_id,
|
|
3924
|
+
at: ctx.now().toISOString(),
|
|
3925
|
+
storyId: story ?? "",
|
|
3926
|
+
});
|
|
3927
|
+
try {
|
|
3928
|
+
appendProjectFocusOverride(hqRoot, project, company?.slug, focusOverride);
|
|
3929
|
+
lane = casUpdateLane(hqRoot, lane.lane_id, (current) => ({
|
|
3930
|
+
...appendLaneFocusOverride(current, focusOverride),
|
|
3931
|
+
}));
|
|
3932
|
+
}
|
|
3933
|
+
catch (error) {
|
|
3934
|
+
try {
|
|
3935
|
+
removeProjectFocusOverride(hqRoot, project, company?.slug, focusOverride);
|
|
3936
|
+
}
|
|
3937
|
+
catch (cleanupError) {
|
|
3938
|
+
process.stderr.write(`${JSON.stringify({
|
|
3939
|
+
event: "lanes.focus_guard.override_rollback_failed",
|
|
3940
|
+
lane_id: lane.lane_id,
|
|
3941
|
+
error: cleanupError instanceof Error ? cleanupError.name : "UnknownError",
|
|
3942
|
+
})}\n`);
|
|
3943
|
+
}
|
|
3944
|
+
try {
|
|
3945
|
+
casUpdateLane(hqRoot, lane.lane_id, (current) => ({
|
|
3946
|
+
...current,
|
|
3947
|
+
state: "failed",
|
|
3948
|
+
queued_reason: undefined,
|
|
3949
|
+
queued_details: undefined,
|
|
3950
|
+
admission_reserved_at: undefined,
|
|
3951
|
+
terminal_reason: "focus_override_persist_failed",
|
|
3952
|
+
}));
|
|
3953
|
+
}
|
|
3954
|
+
catch (markError) {
|
|
3955
|
+
process.stderr.write(`${JSON.stringify({
|
|
3956
|
+
event: "lanes.focus_guard.lane_fail_mark_failed",
|
|
3957
|
+
lane_id: lane.lane_id,
|
|
3958
|
+
error: markError instanceof Error ? markError.name : "UnknownError",
|
|
3959
|
+
})}\n`);
|
|
3960
|
+
}
|
|
3961
|
+
throw new LanesError("focus_override_persist_failed", "Could not persist the focus override; lane launch was refused", {
|
|
3962
|
+
lane_id: lane.lane_id,
|
|
3963
|
+
error: error instanceof Error ? error.name : "UnknownError",
|
|
3964
|
+
});
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3770
3967
|
if (meshCompany) {
|
|
3771
3968
|
try {
|
|
3772
3969
|
lane = await ensureLaneThreadRecord(ctx, lane);
|
|
@@ -4521,6 +4718,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
|
|
|
4521
4718
|
text: options.text,
|
|
4522
4719
|
project: options.project,
|
|
4523
4720
|
story: options.story,
|
|
4721
|
+
focusOverride: options.focusOverride,
|
|
4524
4722
|
account: options.account,
|
|
4525
4723
|
model: options.model,
|
|
4526
4724
|
effort: options.effort,
|
|
@@ -4735,7 +4933,9 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
|
|
|
4735
4933
|
if (action === "reparent") {
|
|
4736
4934
|
for (const descendantId of changedDescendantIds) {
|
|
4737
4935
|
const ownerDescendant = await readOutpostLaneSnapshot(ctx, outpostId, descendantId);
|
|
4738
|
-
descendants.push(
|
|
4936
|
+
descendants.push(behavior.syncPrimary === false
|
|
4937
|
+
? dispatchedOwnerLane(ctx, descendantId, outpostId, ownerDescendant, behavior)
|
|
4938
|
+
: syncLaneRecordFromOwner(ctx.hqRoot(), descendantId, outpostId, ownerDescendant, undefined, { allowNonTerminal: true }));
|
|
4739
4939
|
}
|
|
4740
4940
|
}
|
|
4741
4941
|
const questionCancellation = action === "resume" && lane.state === "awaiting_input" && behavior.afterFallbackResume
|
|
@@ -5964,8 +6164,15 @@ async function printGrantResult(ctx, hqRoot, updated, hierarchy) {
|
|
|
5964
6164
|
* next `set` or an explicit resume `--effort`/`--worker`.
|
|
5965
6165
|
*/
|
|
5966
6166
|
async function runSet(ctx, laneId, opts) {
|
|
6167
|
+
if (opts.company !== undefined) {
|
|
6168
|
+
if (opts.model !== undefined || opts.effort !== undefined || opts.reason !== undefined) {
|
|
6169
|
+
throw new LanesError("lane_set_company_exclusive", "--company cannot be combined with --model, --effort, or --reason", { lane_id: laneId });
|
|
6170
|
+
}
|
|
6171
|
+
await runRepairCompany(ctx, laneId, { company: opts.company });
|
|
6172
|
+
return;
|
|
6173
|
+
}
|
|
5967
6174
|
if (opts.model === undefined && opts.effort === undefined) {
|
|
5968
|
-
throw new LanesError("lane_set_empty", "hq lanes set needs --model or --effort", { lane_id: laneId });
|
|
6175
|
+
throw new LanesError("lane_set_empty", "hq lanes set needs --company, --model, or --effort", { lane_id: laneId });
|
|
5969
6176
|
}
|
|
5970
6177
|
const reason = opts.reason?.trim();
|
|
5971
6178
|
if (opts.reason !== undefined && !reason) {
|
|
@@ -7312,11 +7519,21 @@ function archiveResumeMessage(hqRoot, laneId, message) {
|
|
|
7312
7519
|
fs.writeFileSync(path.join(dir, `${message.message_id}.resume.jsonl`), `${JSON.stringify(message)}\n`, "utf8");
|
|
7313
7520
|
}
|
|
7314
7521
|
async function runReparent(ctx, laneId, opts) {
|
|
7315
|
-
const hqRoot = ctx.hqRoot();
|
|
7316
7522
|
if (!opts.senior) {
|
|
7317
7523
|
throw new LanesError("missing_senior", "--senior is required", {});
|
|
7318
7524
|
}
|
|
7319
|
-
const
|
|
7525
|
+
const target = await resolveCommandLaneTarget(ctx, laneId);
|
|
7526
|
+
const initial = target.lane;
|
|
7527
|
+
if (target.remoteOnly) {
|
|
7528
|
+
await dispatchOutpostLaneCommand(ctx, initial, "reparent", {
|
|
7529
|
+
senior: opts.senior,
|
|
7530
|
+
reason: opts.reason,
|
|
7531
|
+
}, {
|
|
7532
|
+
syncPrimary: false,
|
|
7533
|
+
expectedOwnerHost: target.ownerHost,
|
|
7534
|
+
});
|
|
7535
|
+
return;
|
|
7536
|
+
}
|
|
7320
7537
|
if (initial.host.kind === "outpost" && !laneRunsHere(initial, reconcileHostFromCtx(ctx))) {
|
|
7321
7538
|
await dispatchOutpostLaneCommand(ctx, initial, "reparent", {
|
|
7322
7539
|
senior: opts.senior,
|
|
@@ -7324,7 +7541,7 @@ async function runReparent(ctx, laneId, opts) {
|
|
|
7324
7541
|
});
|
|
7325
7542
|
return;
|
|
7326
7543
|
}
|
|
7327
|
-
const result = await reparentOnOwner(ctx,
|
|
7544
|
+
const result = await reparentOnOwner(ctx, initial.lane_id, opts);
|
|
7328
7545
|
printJson({
|
|
7329
7546
|
ok: true,
|
|
7330
7547
|
action: "reparent",
|
|
@@ -7579,7 +7796,7 @@ async function repairCompanyOnOwner(ctx, laneId, opts, emitResult = true) {
|
|
|
7579
7796
|
if (current.company !== null) {
|
|
7580
7797
|
if (sameLaneCompany(current.company, company))
|
|
7581
7798
|
return current;
|
|
7582
|
-
throw new LanesError("lane_company_already_set", `
|
|
7799
|
+
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 });
|
|
7583
7800
|
}
|
|
7584
7801
|
return {
|
|
7585
7802
|
...current,
|
|
@@ -7935,7 +8152,7 @@ async function dispatchPrimaryLaneMessage(ctx, laneId, options) {
|
|
|
7935
8152
|
printJson(response);
|
|
7936
8153
|
return response;
|
|
7937
8154
|
}
|
|
7938
|
-
async function dispatchOutpostLaneStop(ctx, lane, reason, quiet = false) {
|
|
8155
|
+
async function dispatchOutpostLaneStop(ctx, lane, reason, quiet = false, behavior = {}) {
|
|
7939
8156
|
if (lane.host.kind !== "outpost") {
|
|
7940
8157
|
throw new LanesError("outpost_dispatch_failed", `Lane ${lane.lane_id} has no Outpost host to dispatch stop to`, { lane_id: lane.lane_id });
|
|
7941
8158
|
}
|
|
@@ -7967,6 +8184,14 @@ async function dispatchOutpostLaneStop(ctx, lane, reason, quiet = false) {
|
|
|
7967
8184
|
const commandId = dispatched.commandId?.trim();
|
|
7968
8185
|
const ownerError = ownerErrorFromOutput(dispatched.stdout);
|
|
7969
8186
|
if (isDefiniteOutpostLaneNotFound(dispatched, lane.lane_id, ownerError)) {
|
|
8187
|
+
if (behavior.syncPrimary === false) {
|
|
8188
|
+
throw new LanesError("lane_not_found", ownerError.message, {
|
|
8189
|
+
...ownerError.details,
|
|
8190
|
+
lane_id: lane.lane_id,
|
|
8191
|
+
outpost_id: outpostId,
|
|
8192
|
+
command_id: commandId,
|
|
8193
|
+
});
|
|
8194
|
+
}
|
|
7970
8195
|
const at = ctx.now().toISOString();
|
|
7971
8196
|
const updated = markRemoteLaneLost(ctx.hqRoot(), lane.lane_id, outpostId, at, "command", reason);
|
|
7972
8197
|
if (updated.terminal_reason !== "remote_lost") {
|
|
@@ -8081,7 +8306,9 @@ async function dispatchOutpostLaneStop(ctx, lane, reason, quiet = false) {
|
|
|
8081
8306
|
state = ownerLane.state;
|
|
8082
8307
|
cancelledQuestions = parseRemoteCancelledQuestionIds(remote.cancelled_questions, lane.lane_id, outpostId, commandId);
|
|
8083
8308
|
}
|
|
8084
|
-
const updated =
|
|
8309
|
+
const updated = behavior.syncPrimary === false
|
|
8310
|
+
? ownerLane
|
|
8311
|
+
: syncLaneRecordFromOwner(ctx.hqRoot(), lane.lane_id, outpostId, ownerLane);
|
|
8085
8312
|
if (!quiet)
|
|
8086
8313
|
printJson({
|
|
8087
8314
|
ok: true,
|
|
@@ -8109,9 +8336,14 @@ async function runStop(ctx, laneId, reason, quiet = false) {
|
|
|
8109
8336
|
if (operatorReason !== undefined)
|
|
8110
8337
|
assertNoSecretText("stop reason", operatorReason);
|
|
8111
8338
|
const hqRoot = ctx.hqRoot();
|
|
8112
|
-
const
|
|
8339
|
+
const target = await resolveCommandLaneTarget(ctx, laneId);
|
|
8340
|
+
const lane = target.lane;
|
|
8113
8341
|
if (!quiet && lane.state !== "queued" && !isTerminalLaneState(lane.state)) {
|
|
8114
|
-
process.stderr.write(stopTerminalWarning(
|
|
8342
|
+
process.stderr.write(stopTerminalWarning(lane.lane_id));
|
|
8343
|
+
}
|
|
8344
|
+
if (target.remoteOnly) {
|
|
8345
|
+
await dispatchOutpostLaneStop(ctx, lane, operatorReason, quiet, { syncPrimary: false });
|
|
8346
|
+
return;
|
|
8115
8347
|
}
|
|
8116
8348
|
if (lane.host.kind === "outpost" && !laneRunsHereForOperatorMutation(ctx, lane)) {
|
|
8117
8349
|
await dispatchOutpostLaneStop(ctx, lane, operatorReason, quiet);
|
|
@@ -8885,6 +9117,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8885
9117
|
casUpdateLane,
|
|
8886
9118
|
dispatchOutpostLaneCommand,
|
|
8887
9119
|
dispatchPrimaryLaneMessage,
|
|
9120
|
+
focusGuardAssessment,
|
|
8888
9121
|
drainHookStatusFor,
|
|
8889
9122
|
laneIsHostedHere,
|
|
8890
9123
|
laneWithRecoveredProviderSession,
|
|
@@ -8916,6 +9149,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8916
9149
|
automaticBalanceMoveHistoryReason,
|
|
8917
9150
|
authorizeFailedResume,
|
|
8918
9151
|
authorizeResumeProfileRepin,
|
|
9152
|
+
focusGuardAssessment,
|
|
8919
9153
|
casUpdateLane,
|
|
8920
9154
|
dispatchOutpostLaneCommand: async (commandCtx, lane, action, options, behavior) => {
|
|
8921
9155
|
await dispatchOutpostLaneCommand(commandCtx, lane, action, options, behavior);
|
|
@@ -9031,6 +9265,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9031
9265
|
.option("--mesh-company <slug|uid>", "Explicit Work Mesh company for a personal lane; membership is verified")
|
|
9032
9266
|
.requiredOption("--project <slug>", "HQ project slug")
|
|
9033
9267
|
.option("--story <id>", "Story id from the project's prd.json")
|
|
9268
|
+
.option("--focus-override <reason>", "Human-approved reason to override the lane focus guard")
|
|
9034
9269
|
.addOption(noStoryOption)
|
|
9035
9270
|
.option("--additional-story <id>", "Additional story carried by this lane; repeatable and discouraged when active", collectRepeatable)
|
|
9036
9271
|
.requiredOption("--brief-file <path>", "Path to the lane brief")
|
|
@@ -9093,8 +9328,9 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9093
9328
|
}));
|
|
9094
9329
|
lanes
|
|
9095
9330
|
.command("set")
|
|
9096
|
-
.description("Change
|
|
9331
|
+
.description("Change lane settings or bind its missing company attribution")
|
|
9097
9332
|
.argument("<lane-id>", "Lane id")
|
|
9333
|
+
.option("--company <slug>", "Bind missing company attribution to this lane")
|
|
9098
9334
|
.option("--model <model>", "Model for the lane's next and later turns")
|
|
9099
9335
|
.option("--effort <level>", "Effort for the lane's next and later turns")
|
|
9100
9336
|
.option("--reason <text>", "Why the settings changed; recorded in settings_history")
|
|
@@ -9305,6 +9541,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9305
9541
|
.option("--text-file <path>", "Read message text from a file")
|
|
9306
9542
|
.option("--project <slug>", "Re-point project attribution; a changed story is named in the resumed prompt")
|
|
9307
9543
|
.option("--story <id>", "Re-point story attribution; a changed story is named in the resumed prompt")
|
|
9544
|
+
.option("--focus-override <reason>", "Human-approved reason to override the lane focus guard")
|
|
9308
9545
|
.option("--account <id>", "Move the lane's session to this account of its provider before the message resumes it")
|
|
9309
9546
|
.option("--model <id>", "Model for the moved lane (with --account only)")
|
|
9310
9547
|
.option("--effort <level>", "Reasoning effort for the resumed provider turn")
|
|
@@ -9361,6 +9598,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9361
9598
|
.option("--reason <text>", "Required explanation for resuming from failed")
|
|
9362
9599
|
.option("--project <slug>", "Re-point project attribution; a changed story is named in the resumed prompt")
|
|
9363
9600
|
.option("--story <id>", "Re-point story attribution; a changed story is named in the resumed prompt")
|
|
9601
|
+
.option("--focus-override <reason>", "Human-approved reason to override the lane focus guard")
|
|
9364
9602
|
.option("--account <id>", "Move the lane's session to this account of its provider before resuming")
|
|
9365
9603
|
.option("--model <id>", "Model for the moved lane (with --account only)")
|
|
9366
9604
|
.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",
|
|
@@ -10,6 +10,17 @@ export interface ReadHqAnywherePersonSettingOptions {
|
|
|
10
10
|
}
|
|
11
11
|
/** Returns null when the person preference cannot be read; callers must fail closed. */
|
|
12
12
|
export declare function readHqAnywherePersonSetting(options?: ReadHqAnywherePersonSettingOptions): Promise<boolean | null>;
|
|
13
|
+
/**
|
|
14
|
+
* Wrap a setting reader so a failed read (null) returns the last successfully
|
|
15
|
+
* read value for `graceMs`. A long-running caller such as hqd re-reads every
|
|
16
|
+
* minute, and one slow request would otherwise switch HQ Anywhere off for
|
|
17
|
+
* every session. A newly read value, including off, always applies at once;
|
|
18
|
+
* with no successful read inside the window the result stays null (closed).
|
|
19
|
+
*/
|
|
20
|
+
export declare function stickyPersonSettingReader(read: () => Promise<boolean | null>, options: {
|
|
21
|
+
graceMs: number;
|
|
22
|
+
now?: () => number;
|
|
23
|
+
}): () => Promise<boolean | null>;
|
|
13
24
|
export declare function evaluateAnywhereRuntime(flagEnabled: boolean, setting: boolean | null): {
|
|
14
25
|
enabled: boolean;
|
|
15
26
|
setting: boolean | null;
|
|
@@ -28,6 +28,25 @@ export async function readHqAnywherePersonSetting(options = {}) {
|
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Wrap a setting reader so a failed read (null) returns the last successfully
|
|
33
|
+
* read value for `graceMs`. A long-running caller such as hqd re-reads every
|
|
34
|
+
* minute, and one slow request would otherwise switch HQ Anywhere off for
|
|
35
|
+
* every session. A newly read value, including off, always applies at once;
|
|
36
|
+
* with no successful read inside the window the result stays null (closed).
|
|
37
|
+
*/
|
|
38
|
+
export function stickyPersonSettingReader(read, options) {
|
|
39
|
+
const now = options.now ?? Date.now;
|
|
40
|
+
let last = null;
|
|
41
|
+
return async () => {
|
|
42
|
+
const value = await read();
|
|
43
|
+
if (value !== null) {
|
|
44
|
+
last = { value, at: now() };
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
return last && now() - last.at <= options.graceMs ? last.value : null;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
31
50
|
export function evaluateAnywhereRuntime(flagEnabled, setting) {
|
|
32
51
|
return { enabled: flagEnabled && setting === true, setting };
|
|
33
52
|
}
|
|
@@ -13,7 +13,16 @@ export interface McpToolResult {
|
|
|
13
13
|
}
|
|
14
14
|
export declare function jsonResult(value: unknown): McpToolResult;
|
|
15
15
|
export declare function resolveHqMcpEndpoint(env?: NodeJS.ProcessEnv): URL;
|
|
16
|
-
|
|
16
|
+
/** Test seams for {@link defaultMcpToken}. */
|
|
17
|
+
export interface McpTokenDeps {
|
|
18
|
+
tokenSource?: () => string;
|
|
19
|
+
mcpAudienceToken?: () => Promise<string>;
|
|
20
|
+
cognitoToken?: (options: {
|
|
21
|
+
tokenSource: "person" | "machine";
|
|
22
|
+
interactive: false;
|
|
23
|
+
}) => Promise<string>;
|
|
24
|
+
}
|
|
25
|
+
export declare function defaultMcpToken(deps?: McpTokenDeps): Promise<string>;
|
|
17
26
|
export interface HqMcpBridgeOptions {
|
|
18
27
|
endpoint?: URL;
|
|
19
28
|
env?: NodeJS.ProcessEnv;
|
|
@@ -27,24 +27,31 @@ export function resolveHqMcpEndpoint(env = process.env) {
|
|
|
27
27
|
throw error;
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
export async function defaultMcpToken() {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
export async function defaultMcpToken(deps = {}) {
|
|
31
|
+
const session = deps.tokenSource && deps.cognitoToken ? null : await import("../../../utils/cognito-session.js");
|
|
32
|
+
const tokenSource = deps.tokenSource ?? session.resolveCognitoTokenSource;
|
|
33
|
+
const cognitoToken = deps.cognitoToken ?? ((options) => session.ensureCognitoToken(options));
|
|
34
|
+
if (tokenSource() !== "machine") {
|
|
35
|
+
const mcpAudienceToken = deps.mcpAudienceToken ?? (async () => (await import("../mcp-oauth.js")).getMcpAudienceToken());
|
|
34
36
|
try {
|
|
35
|
-
return await
|
|
37
|
+
return await mcpAudienceToken();
|
|
36
38
|
}
|
|
37
|
-
catch
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
catch {
|
|
40
|
+
// No `hq mcp login`: hosted hq-mcp also accepts the HQ sign-in client,
|
|
41
|
+
// so reuse the session hq-cli and the desktop app already hold.
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
return await cognitoToken({ tokenSource: "person", interactive: false });
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
throw new McpOpError("hq_mcp_auth_required", "Sign in to HQ with `hq login`, then retry.");
|
|
41
48
|
}
|
|
42
49
|
}
|
|
43
50
|
try {
|
|
44
51
|
// Send this box's existing machine token. The hosted hq-mcp audience check
|
|
45
52
|
// accepts it only when it was issued for hqMcpMachineClient; otherwise the
|
|
46
53
|
// server rejects it. This helper does not inspect the credential's client ID.
|
|
47
|
-
return await
|
|
54
|
+
return await cognitoToken({ tokenSource: "machine", interactive: false });
|
|
48
55
|
}
|
|
49
56
|
catch {
|
|
50
57
|
throw new McpOpError("hq_mcp_auth_required", "An hq-mcp machine token is unavailable. Configure this box with hq-pro's hqMcpMachineClient ID, then retry the daemon call.");
|
package/dist/lib/daemon/run.js
CHANGED
|
@@ -14,7 +14,7 @@ import { fileURLToPath } from "node:url";
|
|
|
14
14
|
import { loadCachedTokens } from "@indigoai-us/hq-cloud";
|
|
15
15
|
import { CLI_VERSION } from "../../cli-version.js";
|
|
16
16
|
import { anywhereRuntimeEnabled } from "../../commands/anywhere-runtime-flag.js";
|
|
17
|
-
import { anywhereRuntimeAllowed } from "../anywhere-person-setting.js";
|
|
17
|
+
import { anywhereRuntimeAllowed, readHqAnywherePersonSetting, stickyPersonSettingReader } from "../anywhere-person-setting.js";
|
|
18
18
|
import { currentPackageUseLeaseInstall, currentPackageUseUpdateRequested } from "../../utils/package-use-lease.js";
|
|
19
19
|
import { isLocalDependencyInstall, resolveRunningInstall } from "../../utils/version-gate.js";
|
|
20
20
|
import { agentKitPaths } from "../agent-kit/paths.js";
|
|
@@ -77,6 +77,8 @@ export function handoffForPackageUpdateRequest(requested, restartDaemon) {
|
|
|
77
77
|
const LEGACY_PROBE_TTL_MS = 60_000;
|
|
78
78
|
/** A flag read that has not answered by then counts as off. */
|
|
79
79
|
const HANDOFF_FLAG_TIMEOUT_MS = 10_000;
|
|
80
|
+
/** How long hqd keeps the last read HQ Anywhere setting when a re-read fails. */
|
|
81
|
+
const HQD_PERSON_SETTING_GRACE_MS = 30 * 60_000;
|
|
80
82
|
export function syncRunnerSourceForPlan(stagedRunnerPath, hqCloud, legacyBundledRunnerPath = null, staged = { version: null }, currentSyncRunnerSource = null) {
|
|
81
83
|
// A managed copy lives in the daemon directory, so it survives a CLI
|
|
82
84
|
// replacement. Before lease release pickHqCloud returns it only when it is
|
|
@@ -838,13 +840,17 @@ export async function runDaemon(opts) {
|
|
|
838
840
|
// hqd: the session/company socket every runtime talks to (hq-anywhere US-008).
|
|
839
841
|
let hqdShutdown = null;
|
|
840
842
|
let stopHeartbeat = () => { };
|
|
843
|
+
const readPersonSetting = stickyPersonSettingReader(() => readHqAnywherePersonSetting({
|
|
844
|
+
env,
|
|
845
|
+
onError: (name) => log("warn", `hqd person setting read failed (${name}); keeping the last read value for up to ${HQD_PERSON_SETTING_GRACE_MS / 60_000} minutes`),
|
|
846
|
+
}), { graceMs: HQD_PERSON_SETTING_GRACE_MS });
|
|
841
847
|
const hqdListener = await startHqdRuntimeListener({
|
|
842
848
|
isEnabled: async () => shouldStartHqd(await anywhereRuntimeAllowed(env, {
|
|
843
849
|
refreshFlag: async () => {
|
|
844
850
|
const { refreshFlagRegistrySnapshot } = await import("../flag-registry.js");
|
|
845
851
|
await refreshFlagRegistrySnapshot(5_000);
|
|
846
852
|
},
|
|
847
|
-
|
|
853
|
+
readSetting: readPersonSetting,
|
|
848
854
|
})),
|
|
849
855
|
startServer: async () => {
|
|
850
856
|
const startConfig = readDaemonConfig(paths.configPath);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { CheckResult } from "../types.js";
|
|
2
2
|
export declare const SYNC_RUNTIME_WINDOW_MS: number;
|
|
3
|
+
/** Ignore timing and restart signals until a newly started receiver has settled. */
|
|
4
|
+
export declare const SYNC_RUNTIME_RESTART_WARMUP_MS: number;
|
|
3
5
|
export interface SyncRuntimeOptions {
|
|
4
6
|
now: number;
|
|
5
7
|
conflictCount?: number;
|