@indigoai-us/hq-cli 5.191.0 → 5.192.1
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 +23 -0
- package/dist/command-catalog.generated.d.ts +18 -3
- package/dist/command-catalog.generated.js +24 -3
- package/dist/commands/agents.d.ts +3 -0
- package/dist/commands/agents.js +95 -3
- package/dist/commands/lanes.js +190 -3
- package/dist/commands/report.js +44 -1
- package/dist/lib/agents/model-catalog.d.ts +61 -0
- package/dist/lib/agents/model-catalog.js +275 -0
- package/dist/lib/lanes/admission.js +2 -0
- package/dist/lib/lanes/index.d.ts +2 -2
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/outpost-dispatch.d.ts +7 -0
- package/dist/lib/lanes/outpost-dispatch.js +18 -0
- package/dist/lib/lanes/reconcile.d.ts +10 -1
- package/dist/lib/lanes/reconcile.js +112 -6
- package/dist/lib/lanes/spawn.js +7 -3
- package/dist/lib/lanes/types.d.ts +2 -1
- package/dist/lib/report-publish.d.ts +3 -2
- package/dist/lib/report-publish.js +4 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.192.1] — 2026-09-25
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `hq lanes questions answer` now resumes an asking lane as soon as its final
|
|
10
|
+
pending question is answered. When capacity or throttling prevents a start,
|
|
11
|
+
hq-cli queues the lane with an `answered_questions` resume and the answer
|
|
12
|
+
timestamp. It sends Outpost answers to the lane's owner and reports any
|
|
13
|
+
unanswered sibling questions or a named wake error. After the owner confirms
|
|
14
|
+
the answer, the primary's question copy is marked answered as well.
|
|
15
|
+
|
|
16
|
+
## [5.192.0] — 2026-09-25
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `hq agents models` lists cloud-agent catalog entries by provider and supports JSON output.
|
|
21
|
+
Provision and update validate model IDs against the catalog. They use the cached
|
|
22
|
+
catalog when the endpoint is unavailable, and rely on server validation if no cache exists.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- `hq report adopt` checks whether scheduled jobs are enabled for the company before creating a cloud job, and explains when the server rejects a disabled job.
|
|
27
|
+
|
|
5
28
|
## [5.191.0] — 2026-09-25
|
|
6
29
|
|
|
7
30
|
### Fixed
|
|
@@ -3845,6 +3845,21 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
3845
3845
|
readonly description: "Company slug (resolves to companyUid)";
|
|
3846
3846
|
}];
|
|
3847
3847
|
readonly subcommands: readonly [{
|
|
3848
|
+
readonly name: "models";
|
|
3849
|
+
readonly description: "List the model catalog available to cloud agents";
|
|
3850
|
+
readonly aliases: readonly [];
|
|
3851
|
+
readonly hidden: false;
|
|
3852
|
+
readonly usage: "[options]";
|
|
3853
|
+
readonly arguments: readonly [];
|
|
3854
|
+
readonly options: readonly [{
|
|
3855
|
+
readonly flags: "--provider <provider>";
|
|
3856
|
+
readonly description: "Filter to one provider: codex, grok, or claude";
|
|
3857
|
+
}, {
|
|
3858
|
+
readonly flags: "--json";
|
|
3859
|
+
readonly description: "Print the stable catalog JSON shape";
|
|
3860
|
+
}];
|
|
3861
|
+
readonly subcommands: readonly [];
|
|
3862
|
+
}, {
|
|
3848
3863
|
readonly name: "message";
|
|
3849
3864
|
readonly description: "Send a message to an agent and wait for its reply";
|
|
3850
3865
|
readonly aliases: readonly ["msg"];
|
|
@@ -3939,7 +3954,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
3939
3954
|
readonly description: "Runtime: agents-v2 (recommended) | codex | grok | claude (accepted for script compatibility; use agents-v2 for new Claude agents)";
|
|
3940
3955
|
}, {
|
|
3941
3956
|
readonly flags: "--model <model>";
|
|
3942
|
-
readonly description: "Brain model
|
|
3957
|
+
readonly description: "Brain model from `hq agents models`";
|
|
3943
3958
|
}, {
|
|
3944
3959
|
readonly flags: "--auth-mode <mode>";
|
|
3945
3960
|
readonly description: "Auth: subscription | apiKey (default subscription)";
|
|
@@ -4074,7 +4089,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
4074
4089
|
}, {
|
|
4075
4090
|
readonly name: "config";
|
|
4076
4091
|
readonly description: "Update an agent's runtime config (model / reasoning effort / service tier), or migrate its brain/runtime provider with --provider (DESTRUCTIVE: terminates + reprovisions the box)";
|
|
4077
|
-
readonly aliases: readonly [];
|
|
4092
|
+
readonly aliases: readonly ["update"];
|
|
4078
4093
|
readonly hidden: false;
|
|
4079
4094
|
readonly usage: "[options] <agentUid>";
|
|
4080
4095
|
readonly arguments: readonly [{
|
|
@@ -4087,7 +4102,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
4087
4102
|
readonly description: "Company slug (resolves to companyUid)";
|
|
4088
4103
|
}, {
|
|
4089
4104
|
readonly flags: "--model <model>";
|
|
4090
|
-
readonly description: "
|
|
4105
|
+
readonly description: "Model id from `hq agents models`";
|
|
4091
4106
|
}, {
|
|
4092
4107
|
readonly flags: "--effort <effort>";
|
|
4093
4108
|
readonly description: "Reasoning effort: minimal | low | medium | high | xhigh";
|
|
@@ -4961,6 +4961,25 @@ export const COMMAND_CATALOG = [
|
|
|
4961
4961
|
}
|
|
4962
4962
|
],
|
|
4963
4963
|
"subcommands": [
|
|
4964
|
+
{
|
|
4965
|
+
"name": "models",
|
|
4966
|
+
"description": "List the model catalog available to cloud agents",
|
|
4967
|
+
"aliases": [],
|
|
4968
|
+
"hidden": false,
|
|
4969
|
+
"usage": "[options]",
|
|
4970
|
+
"arguments": [],
|
|
4971
|
+
"options": [
|
|
4972
|
+
{
|
|
4973
|
+
"flags": "--provider <provider>",
|
|
4974
|
+
"description": "Filter to one provider: codex, grok, or claude"
|
|
4975
|
+
},
|
|
4976
|
+
{
|
|
4977
|
+
"flags": "--json",
|
|
4978
|
+
"description": "Print the stable catalog JSON shape"
|
|
4979
|
+
}
|
|
4980
|
+
],
|
|
4981
|
+
"subcommands": []
|
|
4982
|
+
},
|
|
4964
4983
|
{
|
|
4965
4984
|
"name": "message",
|
|
4966
4985
|
"description": "Send a message to an agent and wait for its reply",
|
|
@@ -5092,7 +5111,7 @@ export const COMMAND_CATALOG = [
|
|
|
5092
5111
|
},
|
|
5093
5112
|
{
|
|
5094
5113
|
"flags": "--model <model>",
|
|
5095
|
-
"description": "Brain model
|
|
5114
|
+
"description": "Brain model from `hq agents models`"
|
|
5096
5115
|
},
|
|
5097
5116
|
{
|
|
5098
5117
|
"flags": "--auth-mode <mode>",
|
|
@@ -5268,7 +5287,9 @@ export const COMMAND_CATALOG = [
|
|
|
5268
5287
|
{
|
|
5269
5288
|
"name": "config",
|
|
5270
5289
|
"description": "Update an agent's runtime config (model / reasoning effort / service tier), or migrate its brain/runtime provider with --provider (DESTRUCTIVE: terminates + reprovisions the box)",
|
|
5271
|
-
"aliases": [
|
|
5290
|
+
"aliases": [
|
|
5291
|
+
"update"
|
|
5292
|
+
],
|
|
5272
5293
|
"hidden": false,
|
|
5273
5294
|
"usage": "[options] <agentUid>",
|
|
5274
5295
|
"arguments": [
|
|
@@ -5285,7 +5306,7 @@ export const COMMAND_CATALOG = [
|
|
|
5285
5306
|
},
|
|
5286
5307
|
{
|
|
5287
5308
|
"flags": "--model <model>",
|
|
5288
|
-
"description": "
|
|
5309
|
+
"description": "Model id from `hq agents models`"
|
|
5289
5310
|
},
|
|
5290
5311
|
{
|
|
5291
5312
|
"flags": "--effort <effort>",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import { Command } from "commander";
|
|
30
30
|
import { type TerminalSessionPayload } from "@indigoai-us/hq-cloud/outposts/node";
|
|
31
|
+
import { type AgentModelProvider, type AgentModelCatalog } from "../lib/agents/model-catalog.js";
|
|
31
32
|
import { type BillingErrorPayload } from "../utils/billing-gate.js";
|
|
32
33
|
/** Reasoning-effort values hq-pro accepts on `runtime-config`. */
|
|
33
34
|
export declare const VALID_EFFORTS: Set<string>;
|
|
@@ -341,6 +342,8 @@ export declare function formatJobsTable(jobs: AgentJobView[]): string;
|
|
|
341
342
|
export declare function formatPauseResult(result: PauseJobResult): string;
|
|
342
343
|
/** Human-readable "hot-applied to the running box" vs "saved for next launch". */
|
|
343
344
|
export declare function appliedHint(applied: boolean): string;
|
|
345
|
+
/** Stable human table shared by the all-provider and provider-filtered forms. */
|
|
346
|
+
export declare function formatAgentModelsTable(catalog: AgentModelCatalog, selectedProvider?: AgentModelProvider): string;
|
|
344
347
|
/** Agent uid shape (`agt_…`). */
|
|
345
348
|
/** One message in a DM thread, as returned by `GET /v1/notify/thread`. */
|
|
346
349
|
export interface DmThreadMessage {
|
package/dist/commands/agents.js
CHANGED
|
@@ -38,6 +38,7 @@ import { launchSessionManagerPlugin, SessionManagerPluginLaunchError, TerminalSe
|
|
|
38
38
|
import { peekIdToken } from "../utils/id-token.js";
|
|
39
39
|
import { canOfferTeamUpgrade, formatUpgradeRefusal, isPlanGateError, offerUpgradeLink, } from "../utils/plan-gate-error.js";
|
|
40
40
|
import { AGENT_UID_PATTERN, isAgentUid } from "../lib/notify-recipient.js";
|
|
41
|
+
import { AGENT_MODEL_PROVIDERS, modelSelectionErrorMessage, readAgentModelCatalog, validateAgentModelSelection, } from "../lib/agents/model-catalog.js";
|
|
41
42
|
import { confirmChargeOrExit, formatUsd, parseBillingPayload, surfaceBillingBlocked, } from "../utils/billing-gate.js";
|
|
42
43
|
/** Reasoning-effort values hq-pro accepts on `runtime-config`. */
|
|
43
44
|
export const VALID_EFFORTS = new Set([
|
|
@@ -57,11 +58,12 @@ export const VALID_AUTH_MODES = new Set(["subscription", "apiKey"]);
|
|
|
57
58
|
export const VALID_AGENT_SIZE_KEYS = new Set(["basic", "power", "dev"]);
|
|
58
59
|
// Keep cloud agent create slugs aligned with hq-pro-agents POST /v1/agents.
|
|
59
60
|
const AGENT_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
60
|
-
const CLAUDE_PROVISION_COMMAND = "hq agents provision <name> --provider agents-v2 --model
|
|
61
|
+
const CLAUDE_PROVISION_COMMAND = "hq agents provision <name> --provider agents-v2 --model <model-id>";
|
|
61
62
|
const CLAUDE_API_KEY_ARGS = "--auth-mode apiKey --api-key-env <VAR>";
|
|
62
63
|
function claudeProvisionGuidance() {
|
|
63
64
|
return [
|
|
64
65
|
"The account must be enabled for Claude. Subscription auth is the default.",
|
|
66
|
+
"Use `hq agents models` to see the current model ids.",
|
|
65
67
|
`Create an agent with: ${CLAUDE_PROVISION_COMMAND}`,
|
|
66
68
|
`For API-key auth, add: ${CLAUDE_API_KEY_ARGS}`,
|
|
67
69
|
"The CLI accepts --provider claude for script compatibility, but the server rejects new agents that use it.",
|
|
@@ -863,6 +865,50 @@ export function appliedHint(applied) {
|
|
|
863
865
|
? "applied to the running box now"
|
|
864
866
|
: "saved — takes effect on the agent's next launch";
|
|
865
867
|
}
|
|
868
|
+
async function validateAgentModelOption(token, chosenProvider, modelId) {
|
|
869
|
+
const result = await readAgentModelCatalog(token);
|
|
870
|
+
if (!result.ok) {
|
|
871
|
+
if (result.reason === "unavailable") {
|
|
872
|
+
console.error(chalk.yellow("model validation skipped: catalog unavailable (the server still validates)."));
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
throw new Error(`Could not read the model catalog (HTTP ${result.status}). Check your hq login and try again.`);
|
|
876
|
+
}
|
|
877
|
+
if (result.source === "cache") {
|
|
878
|
+
console.error(chalk.yellow(`Model catalog unavailable; validating against the cached catalog from ${new Date(result.fetchedAt).toISOString()}.`));
|
|
879
|
+
}
|
|
880
|
+
const selection = validateAgentModelSelection(result.catalog, modelId, chosenProvider);
|
|
881
|
+
if (!selection.ok) {
|
|
882
|
+
throw new Error(modelSelectionErrorMessage(modelId, selection));
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
/** Stable human table shared by the all-provider and provider-filtered forms. */
|
|
886
|
+
export function formatAgentModelsTable(catalog, selectedProvider) {
|
|
887
|
+
const providers = selectedProvider ? [selectedProvider] : AGENT_MODEL_PROVIDERS;
|
|
888
|
+
const sections = providers.map((provider) => {
|
|
889
|
+
const view = catalog.providers[provider];
|
|
890
|
+
const columns = ["ID", "LABEL", "DEFAULT", "NEW", "AVAILABILITY"];
|
|
891
|
+
const rows = view.models.map((model) => [
|
|
892
|
+
model.id,
|
|
893
|
+
model.label,
|
|
894
|
+
model.id === view.defaultModel ? "yes" : "",
|
|
895
|
+
model.isNew ? "yes" : "",
|
|
896
|
+
model.selectable ? "available" : "not yet on agents",
|
|
897
|
+
]);
|
|
898
|
+
const widths = columns.map((column, index) => Math.max(column.length, ...rows.map((row) => row[index].length)));
|
|
899
|
+
const render = (cells) => cells.map((cell, index) => cell.padEnd(widths[index])).join(" ").trimEnd();
|
|
900
|
+
return [
|
|
901
|
+
chalk.bold(provider.toUpperCase()),
|
|
902
|
+
render(columns),
|
|
903
|
+
...rows.map(render),
|
|
904
|
+
].join("\n");
|
|
905
|
+
});
|
|
906
|
+
return sections.join("\n\n");
|
|
907
|
+
}
|
|
908
|
+
function catalogUnavailableMessage() {
|
|
909
|
+
return ("Model catalog is unavailable and no cached copy exists. " +
|
|
910
|
+
"Check your connection and try again after model-catalog.serve is enabled.");
|
|
911
|
+
}
|
|
866
912
|
/**
|
|
867
913
|
* Resolve an agent reference to its `agt_` uid. An `agt_…` value is used
|
|
868
914
|
* directly; anything else is treated as a slug or display name and resolved
|
|
@@ -1097,6 +1143,8 @@ export function registerAgentsCommand(program) {
|
|
|
1097
1143
|
bySubcommand: {
|
|
1098
1144
|
message: { capability: "agents:use", routeAvailable: false },
|
|
1099
1145
|
thread: { capability: "agents:use", routeAvailable: false },
|
|
1146
|
+
// The catalog read endpoint is JWT-only; hq-pro has no keyed parallel.
|
|
1147
|
+
models: { capability: "agents:use", routeAvailable: false },
|
|
1100
1148
|
// Interactive terminals are for humans: hq-pro serves no keyed
|
|
1101
1149
|
// `/v1/keys/agents/{uid}/terminal` route, so machine keys fail closed.
|
|
1102
1150
|
terminal: { capability: "agents:use", routeAvailable: false },
|
|
@@ -1112,6 +1160,43 @@ export function registerAgentsCommand(program) {
|
|
|
1112
1160
|
// `hq agents --company acme list`.
|
|
1113
1161
|
const companyOf = (sub) => sub.opts().company ??
|
|
1114
1162
|
agents.opts().company;
|
|
1163
|
+
agents
|
|
1164
|
+
.command("models")
|
|
1165
|
+
.description("List the model catalog available to cloud agents")
|
|
1166
|
+
.option("--provider <provider>", "Filter to one provider: codex, grok, or claude")
|
|
1167
|
+
.option("--json", "Print the stable catalog JSON shape")
|
|
1168
|
+
.action(async (opts) => {
|
|
1169
|
+
try {
|
|
1170
|
+
const token = (await resolveVaultCredential()).token;
|
|
1171
|
+
const result = await readAgentModelCatalog(token);
|
|
1172
|
+
if (!result.ok) {
|
|
1173
|
+
if (result.reason === "request_failed") {
|
|
1174
|
+
throw new Error(`Could not read the model catalog (HTTP ${result.status}). Check your hq login and try again.`);
|
|
1175
|
+
}
|
|
1176
|
+
console.error(chalk.red(catalogUnavailableMessage()));
|
|
1177
|
+
process.exit(1);
|
|
1178
|
+
}
|
|
1179
|
+
if (result.source === "cache") {
|
|
1180
|
+
console.error(chalk.yellow(`Model catalog unavailable; showing cached catalog from ${new Date(result.fetchedAt).toISOString()}.`));
|
|
1181
|
+
}
|
|
1182
|
+
const providers = opts.provider
|
|
1183
|
+
? { [opts.provider]: result.catalog.providers[opts.provider] }
|
|
1184
|
+
: result.catalog.providers;
|
|
1185
|
+
if (opts.json) {
|
|
1186
|
+
// Stable shape: { generatedAt, providers }. A provider filter selects
|
|
1187
|
+
// one key but never changes or flattens the response envelope.
|
|
1188
|
+
process.stdout.write(JSON.stringify({ generatedAt: result.catalog.generatedAt, providers }, null, 2) + "\n");
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
console.log(formatAgentModelsTable(result.catalog, opts.provider));
|
|
1192
|
+
}
|
|
1193
|
+
catch (err) {
|
|
1194
|
+
fail(err);
|
|
1195
|
+
}
|
|
1196
|
+
});
|
|
1197
|
+
agents.commands.find((command) => command.name() === "models")
|
|
1198
|
+
?.options.find((option) => option.long === "--provider")
|
|
1199
|
+
?.choices(AGENT_MODEL_PROVIDERS);
|
|
1115
1200
|
agents
|
|
1116
1201
|
.command("message <agent> <message...>")
|
|
1117
1202
|
.alias("msg")
|
|
@@ -1269,7 +1354,7 @@ export function registerAgentsCommand(program) {
|
|
|
1269
1354
|
.option("--company <slug>", "Company slug (resolves to companyUid)")
|
|
1270
1355
|
.option("--slug <slug>", "Agent slug (defaults to a slug of <name>)")
|
|
1271
1356
|
.option("--provider <provider>", "Runtime: agents-v2 (recommended) | codex | grok | claude (accepted for script compatibility; use agents-v2 for new Claude agents)")
|
|
1272
|
-
.option("--model <model>", "Brain model
|
|
1357
|
+
.option("--model <model>", "Brain model from `hq agents models`")
|
|
1273
1358
|
.option("--auth-mode <mode>", "Auth: subscription | apiKey (default subscription)", "subscription")
|
|
1274
1359
|
.option("--api-key-env <VAR>", "Env var holding the API key for --auth-mode apiKey (never pass the key as a flag)")
|
|
1275
1360
|
.option("--slack-bot-token <xoxb-token>", "Reuse an existing Slack bot token; HQ_SLACK_BOT_TOKEN or --slack-tokens-stdin avoid shell history")
|
|
@@ -1334,6 +1419,9 @@ export function registerAgentsCommand(program) {
|
|
|
1334
1419
|
}
|
|
1335
1420
|
const token = (await resolveVaultCredential()).token;
|
|
1336
1421
|
const companyUid = await getCompanyUid(token, companyOf(this));
|
|
1422
|
+
if (opts.model !== undefined) {
|
|
1423
|
+
await validateAgentModelOption(token, provider, opts.model);
|
|
1424
|
+
}
|
|
1337
1425
|
const idempotencyKey = `hq-cli-${randomUUID()}`;
|
|
1338
1426
|
const sizeKey = parseEnumOption(opts.size, VALID_AGENT_SIZE_KEYS, "--size");
|
|
1339
1427
|
const shouldChooseInteractively = !sizeKey && process.stdin.isTTY === true;
|
|
@@ -1636,9 +1724,10 @@ export function registerAgentsCommand(program) {
|
|
|
1636
1724
|
});
|
|
1637
1725
|
agents
|
|
1638
1726
|
.command("config <agentUid>")
|
|
1727
|
+
.alias("update")
|
|
1639
1728
|
.description("Update an agent's runtime config (model / reasoning effort / service tier), or migrate its brain/runtime provider with --provider (DESTRUCTIVE: terminates + reprovisions the box)")
|
|
1640
1729
|
.option("--company <slug>", "Company slug (resolves to companyUid)")
|
|
1641
|
-
.option("--model <model>", "
|
|
1730
|
+
.option("--model <model>", "Model id from `hq agents models`")
|
|
1642
1731
|
.option("--effort <effort>", "Reasoning effort: minimal | low | medium | high | xhigh")
|
|
1643
1732
|
.option("--tier <tier>", "Service tier (speed): default | priority")
|
|
1644
1733
|
.option("--provider <provider>", "Migrate brain/runtime provider (codex | grok | claude | agents-v2). DESTRUCTIVE: terminates and reprovisions the box; requires --model and --yes.")
|
|
@@ -1707,6 +1796,9 @@ export function registerAgentsCommand(program) {
|
|
|
1707
1796
|
process.exit(1);
|
|
1708
1797
|
}
|
|
1709
1798
|
const token = (await resolveVaultCredential()).token;
|
|
1799
|
+
if (opts.model !== undefined) {
|
|
1800
|
+
await validateAgentModelOption(token, opts.provider?.trim().toLowerCase(), opts.model);
|
|
1801
|
+
}
|
|
1710
1802
|
const result = await patchAgentRuntimeConfig(token, agentUid, patch);
|
|
1711
1803
|
if (opts.json) {
|
|
1712
1804
|
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
package/dist/commands/lanes.js
CHANGED
|
@@ -21,7 +21,7 @@ import { createOutpostHireGrantContext, parseOutpostHireGrantContext, sameHireGr
|
|
|
21
21
|
import { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity, resolveAdmissionHost, } from "../lib/lanes/admission-host.js";
|
|
22
22
|
export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
23
23
|
export { LANE_HIRE_GRANTS_FLAG };
|
|
24
|
-
import { buildOutpostCapacityCommand, buildOutpostBalanceMoveCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand, buildOutpostMoveTargetCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, buildOutpostLaneListCommand, outpostRejectedEffortOption, parseOutpostPickupPayload, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
|
|
24
|
+
import { buildOutpostCapacityCommand, buildOutpostBalanceMoveCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand, buildOutpostMoveTargetCommand, buildOutpostQuestionAnswerCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, buildOutpostLaneListCommand, outpostRejectedEffortOption, parseOutpostPickupPayload, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
|
|
25
25
|
import { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "../lib/lanes/host-capacity.js";
|
|
26
26
|
import { BALANCE_MOVABLE_STATES, CLAUDE_ACCOUNT_BALANCE_FLAG, accountLoadsForReport, assertBalanceApplyEnabled, planClaudeBalanceMoves, selectClaudeAccount, } from "../lib/lanes/claude-account-balance.js";
|
|
27
27
|
import { parseUserYaml } from "../utils/user-yaml-error.js";
|
|
@@ -30,7 +30,7 @@ import { vaultApiFetch } from "../utils/vault-api.js";
|
|
|
30
30
|
import { openMeshTransport } from "../lib/mesh/client.js";
|
|
31
31
|
import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
|
|
32
32
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
33
|
-
import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseLivenessState, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, withLaneAdmissionLock, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
33
|
+
import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseLivenessState, parseQuestionRecord, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, wakeAnsweredLaneNow, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, withLaneAdmissionLock, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
34
34
|
import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
|
|
35
35
|
import { questionBodyAbsPath } from "../lib/lanes/questions.js";
|
|
36
36
|
import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
@@ -3208,6 +3208,139 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3208
3208
|
outpost_transport: "outposts_exec",
|
|
3209
3209
|
});
|
|
3210
3210
|
}
|
|
3211
|
+
async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
|
|
3212
|
+
if (lane.host.kind !== "outpost") {
|
|
3213
|
+
throw new LanesError("outpost_dispatch_failed", `Lane ${lane.lane_id} has no Outpost host to answer its question`, { lane_id: lane.lane_id, question_id: questionId });
|
|
3214
|
+
}
|
|
3215
|
+
const outpostId = lane.host.id;
|
|
3216
|
+
let dispatched;
|
|
3217
|
+
try {
|
|
3218
|
+
dispatched = await ctx.dispatchOutpostExec({
|
|
3219
|
+
outpostId,
|
|
3220
|
+
command: buildOutpostQuestionAnswerCommand({
|
|
3221
|
+
questionId,
|
|
3222
|
+
choice: options.choice,
|
|
3223
|
+
freeform: options.freeform,
|
|
3224
|
+
notes: options.notes,
|
|
3225
|
+
}),
|
|
3226
|
+
});
|
|
3227
|
+
}
|
|
3228
|
+
catch (err) {
|
|
3229
|
+
throw new LanesError("outpost_dispatch_failed", `Could not dispatch question ${questionId} to Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { lane_id: lane.lane_id, question_id: questionId, outpost_id: outpostId });
|
|
3230
|
+
}
|
|
3231
|
+
if (dispatched.done !== true || dispatched.status !== "Success" || dispatched.exitCode !== 0) {
|
|
3232
|
+
throw new LanesError("outpost_dispatch_failed", `Outpost ${outpostId} did not confirm the answer for question ${questionId}`, {
|
|
3233
|
+
lane_id: lane.lane_id,
|
|
3234
|
+
question_id: questionId,
|
|
3235
|
+
outpost_id: outpostId,
|
|
3236
|
+
command_id: dispatched.commandId,
|
|
3237
|
+
status: dispatched.status,
|
|
3238
|
+
done: dispatched.done ?? false,
|
|
3239
|
+
exit_code: dispatched.exitCode,
|
|
3240
|
+
});
|
|
3241
|
+
}
|
|
3242
|
+
const remote = parseRemoteJson(dispatched.stdout, "Outpost question answer");
|
|
3243
|
+
if (remote?.error !== undefined) {
|
|
3244
|
+
throw new LanesError("outpost_remote_command_failed", typeof remote.message === "string"
|
|
3245
|
+
? remote.message
|
|
3246
|
+
: `Outpost ${outpostId} rejected the answer for question ${questionId}`, {
|
|
3247
|
+
lane_id: lane.lane_id,
|
|
3248
|
+
question_id: questionId,
|
|
3249
|
+
outpost_id: outpostId,
|
|
3250
|
+
command_id: dispatched.commandId,
|
|
3251
|
+
remote_error: remote.error,
|
|
3252
|
+
});
|
|
3253
|
+
}
|
|
3254
|
+
const remoteQuestion = remote?.question !== null && typeof remote?.question === "object"
|
|
3255
|
+
? remote.question
|
|
3256
|
+
: null;
|
|
3257
|
+
const awaitingWakeConfirmed = lane.state !== "awaiting_input" ||
|
|
3258
|
+
(remote?.wake_status === "resumed" && remote.lane_state === "running") ||
|
|
3259
|
+
(remote?.wake_status === "queued" && remote.lane_state === "queued") ||
|
|
3260
|
+
(remote?.wake_status === "awaiting_answers" && remote.lane_state === "awaiting_input");
|
|
3261
|
+
if (remote?.ok !== true ||
|
|
3262
|
+
remote.action !== "questions.answer" ||
|
|
3263
|
+
remoteQuestion?.question_id !== questionId ||
|
|
3264
|
+
remoteQuestion.lane_id !== lane.lane_id ||
|
|
3265
|
+
remoteQuestion.status !== "answered" ||
|
|
3266
|
+
typeof remote.delivery !== "string" ||
|
|
3267
|
+
!awaitingWakeConfirmed) {
|
|
3268
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} completed exec without confirming the answer and wake status for question ${questionId}`, {
|
|
3269
|
+
lane_id: lane.lane_id,
|
|
3270
|
+
question_id: questionId,
|
|
3271
|
+
outpost_id: outpostId,
|
|
3272
|
+
command_id: dispatched.commandId,
|
|
3273
|
+
});
|
|
3274
|
+
}
|
|
3275
|
+
let ownerQuestion;
|
|
3276
|
+
try {
|
|
3277
|
+
ownerQuestion = parseQuestionRecord(remoteQuestion);
|
|
3278
|
+
}
|
|
3279
|
+
catch (err) {
|
|
3280
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return a valid answered question record for ${questionId}: ${dispatchErrorMessage(err)}`, {
|
|
3281
|
+
lane_id: lane.lane_id,
|
|
3282
|
+
question_id: questionId,
|
|
3283
|
+
outpost_id: outpostId,
|
|
3284
|
+
command_id: dispatched.commandId,
|
|
3285
|
+
});
|
|
3286
|
+
}
|
|
3287
|
+
const ownerAnswer = ownerQuestion.answer;
|
|
3288
|
+
if (!isQuestionAnswer(ownerAnswer)) {
|
|
3289
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return the confirmed answer for question ${questionId}`, {
|
|
3290
|
+
lane_id: lane.lane_id,
|
|
3291
|
+
question_id: questionId,
|
|
3292
|
+
outpost_id: outpostId,
|
|
3293
|
+
command_id: dispatched.commandId,
|
|
3294
|
+
});
|
|
3295
|
+
}
|
|
3296
|
+
const syncedQuestion = casUpdateQuestion(ctx.hqRoot(), questionId, (current) => {
|
|
3297
|
+
if (current.lane_id !== lane.lane_id) {
|
|
3298
|
+
throw new LanesError("outpost_question_sync_conflict", `Primary question ${questionId} belongs to a different lane than Outpost ${outpostId} confirmed`, { lane_id: lane.lane_id, question_id: questionId, outpost_id: outpostId });
|
|
3299
|
+
}
|
|
3300
|
+
if (current.status === "pending") {
|
|
3301
|
+
return { ...current, status: "answered", answer: ownerAnswer };
|
|
3302
|
+
}
|
|
3303
|
+
if (current.status === "answered" && sameQuestionAnswer(current.answer, ownerAnswer)) {
|
|
3304
|
+
return current;
|
|
3305
|
+
}
|
|
3306
|
+
throw new LanesError("outpost_question_sync_conflict", `Primary question ${questionId} is ${current.status} or has a different answer than Outpost ${outpostId} confirmed`, { lane_id: lane.lane_id, question_id: questionId, outpost_id: outpostId });
|
|
3307
|
+
});
|
|
3308
|
+
printJson({
|
|
3309
|
+
...remote,
|
|
3310
|
+
lane_id: lane.lane_id,
|
|
3311
|
+
question: syncedQuestion,
|
|
3312
|
+
outpost_id: outpostId,
|
|
3313
|
+
outpost_command_id: dispatched.commandId,
|
|
3314
|
+
outpost_transport: "outposts_exec",
|
|
3315
|
+
});
|
|
3316
|
+
}
|
|
3317
|
+
function isQuestionAnswer(value) {
|
|
3318
|
+
if (value === null || typeof value !== "object")
|
|
3319
|
+
return false;
|
|
3320
|
+
const answer = value;
|
|
3321
|
+
return (typeof answer.choice === "string" &&
|
|
3322
|
+
answer.choice.trim() !== "" &&
|
|
3323
|
+
(answer.answer_kind === undefined ||
|
|
3324
|
+
answer.answer_kind === "option" ||
|
|
3325
|
+
answer.answer_kind === "freeform") &&
|
|
3326
|
+
(answer.notes_ref === undefined || typeof answer.notes_ref === "string") &&
|
|
3327
|
+
typeof answer.answered_by === "string" &&
|
|
3328
|
+
answer.answered_by.trim() !== "" &&
|
|
3329
|
+
typeof answer.answered_at === "string" &&
|
|
3330
|
+
Number.isFinite(Date.parse(answer.answered_at)) &&
|
|
3331
|
+
typeof answer.answered_level === "number" &&
|
|
3332
|
+
Number.isInteger(answer.answered_level) &&
|
|
3333
|
+
answer.answered_level > 0);
|
|
3334
|
+
}
|
|
3335
|
+
function sameQuestionAnswer(left, right) {
|
|
3336
|
+
return (left !== undefined &&
|
|
3337
|
+
left.choice === right.choice &&
|
|
3338
|
+
left.answer_kind === right.answer_kind &&
|
|
3339
|
+
left.notes_ref === right.notes_ref &&
|
|
3340
|
+
left.answered_by === right.answered_by &&
|
|
3341
|
+
left.answered_at === right.answered_at &&
|
|
3342
|
+
left.answered_level === right.answered_level);
|
|
3343
|
+
}
|
|
3211
3344
|
function ingestPickupPayload(hqRoot, payload, now) {
|
|
3212
3345
|
const laneId = payload.lane.lane_id;
|
|
3213
3346
|
if (!laneId) {
|
|
@@ -6464,9 +6597,16 @@ async function runQuestionsShow(ctx, questionId) {
|
|
|
6464
6597
|
async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
6465
6598
|
const hqRoot = ctx.hqRoot();
|
|
6466
6599
|
const question = readQuestion(hqRoot, questionId);
|
|
6467
|
-
throwIfQuestionNotPending(question);
|
|
6468
6600
|
await yieldEventLoop();
|
|
6469
6601
|
const askingLane = readLane(hqRoot, laneIdForAnswerDelivery(question));
|
|
6602
|
+
if (askingLane.host.kind === "outpost" && !laneIsHostedHere(ctx, askingLane)) {
|
|
6603
|
+
await dispatchOutpostQuestionAnswer(ctx, askingLane, questionId, opts);
|
|
6604
|
+
return;
|
|
6605
|
+
}
|
|
6606
|
+
throwIfQuestionNotPending(question);
|
|
6607
|
+
if (askingLane.state === "awaiting_input") {
|
|
6608
|
+
assertLaneRunsHereForOperatorMutation(ctx, askingLane, "wake after answering");
|
|
6609
|
+
}
|
|
6470
6610
|
if (laneAcceptsDropboxDelivery(askingLane)) {
|
|
6471
6611
|
assertLaneRunsHereForOperatorMutation(ctx, askingLane, "deliver an answer to");
|
|
6472
6612
|
}
|
|
@@ -6511,6 +6651,53 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
|
6511
6651
|
},
|
|
6512
6652
|
};
|
|
6513
6653
|
});
|
|
6654
|
+
const latestAskingLane = readLane(hqRoot, askingLane.lane_id);
|
|
6655
|
+
if (latestAskingLane.state === "awaiting_input") {
|
|
6656
|
+
let wake;
|
|
6657
|
+
try {
|
|
6658
|
+
wake = await wakeAnsweredLaneNow(hqRoot, askingLane.lane_id, {
|
|
6659
|
+
now: ctx.now,
|
|
6660
|
+
host: reconcileHostFromCtx(ctx),
|
|
6661
|
+
dryRun: false,
|
|
6662
|
+
spawnLaneDeps: {
|
|
6663
|
+
env: ctx.env,
|
|
6664
|
+
existsSync: ctx.existsSync,
|
|
6665
|
+
spawnProcess: ctx.spawnProcess,
|
|
6666
|
+
proveDetached: ctx.proveDetached,
|
|
6667
|
+
killProcess: ctx.killProcess,
|
|
6668
|
+
now: ctx.now,
|
|
6669
|
+
resolveMachineIdentity: ctx.resolveMachineIdentity,
|
|
6670
|
+
runCompaction: ctx.runCompaction,
|
|
6671
|
+
locateTranscript: ctx.locateTranscript,
|
|
6672
|
+
},
|
|
6673
|
+
throttleDeps: ctx.throttleDeps,
|
|
6674
|
+
livenessDeps: livenessDepsFromCtx(ctx),
|
|
6675
|
+
presenceConnected: ctx.presenceConnected,
|
|
6676
|
+
mesh: ctx.mesh,
|
|
6677
|
+
env: ctx.env,
|
|
6678
|
+
});
|
|
6679
|
+
}
|
|
6680
|
+
catch (err) {
|
|
6681
|
+
throw new LanesError("question_wake_failed", `Question ${questionId} was recorded, but lane ${askingLane.lane_id} could not be woken: ${dispatchErrorMessage(err)}`, { lane_id: askingLane.lane_id, question_id: questionId });
|
|
6682
|
+
}
|
|
6683
|
+
if (wake.status === "failed") {
|
|
6684
|
+
throw new LanesError("question_wake_failed", `Question ${questionId} was recorded, but lane ${askingLane.lane_id} could not be woken: ${wake.error}`, { lane_id: askingLane.lane_id, question_id: questionId, wake_error: wake.error });
|
|
6685
|
+
}
|
|
6686
|
+
printJson({
|
|
6687
|
+
ok: true,
|
|
6688
|
+
action: "questions.answer",
|
|
6689
|
+
lane_id: askingLane.lane_id,
|
|
6690
|
+
question: updated,
|
|
6691
|
+
delivery: wake.status === "awaiting_answers" ? "awaiting_other_answers" : wake.status,
|
|
6692
|
+
wake_status: wake.status,
|
|
6693
|
+
lane_state: wake.lane_state,
|
|
6694
|
+
...(wake.queued_reason !== undefined ? { queued_reason: wake.queued_reason } : {}),
|
|
6695
|
+
...(wake.remaining_question_ids !== undefined
|
|
6696
|
+
? { remaining_question_ids: wake.remaining_question_ids }
|
|
6697
|
+
: {}),
|
|
6698
|
+
});
|
|
6699
|
+
return;
|
|
6700
|
+
}
|
|
6514
6701
|
const delivery = deliverAnswerToAskingLane(ctx, updated, choice, at);
|
|
6515
6702
|
printJson({
|
|
6516
6703
|
ok: true,
|
package/dist/commands/report.js
CHANGED
|
@@ -9,6 +9,7 @@ import { adoptReport, buildReportTarball, conventionReportDir, deployOrgContext,
|
|
|
9
9
|
const DEPLOY_API_URL = process.env.HQ_DEPLOY_API_URL ?? "https://api.indigo-hq.com";
|
|
10
10
|
const REPORT_CHANNEL_STATE_FILE = "report-channels.json";
|
|
11
11
|
const MAX_REPORT_DISCOVERY_CHANNELS = 50;
|
|
12
|
+
const JOB_EXECUTION_DISABLED_MESSAGE = "Scheduled jobs are turned off for this company. A company owner can turn them back on.";
|
|
12
13
|
function reportPath(channelId, reportId) {
|
|
13
14
|
return `/v1/notify/channels/${encodeURIComponent(channelId)}/reports/${encodeURIComponent(reportId)}`;
|
|
14
15
|
}
|
|
@@ -414,10 +415,39 @@ async function cancelOwnedJob(token, agentUid, jobId) {
|
|
|
414
415
|
if (!res.ok && res.status !== 404)
|
|
415
416
|
throw new ReportPublishError("Could not remove the report job.");
|
|
416
417
|
}
|
|
418
|
+
async function requireScheduledJobsEnabled(token, agentUid) {
|
|
419
|
+
let executionEnabled;
|
|
420
|
+
try {
|
|
421
|
+
const res = await vaultApiFetch({
|
|
422
|
+
token,
|
|
423
|
+
path: `/v1/agents/${encodeURIComponent(agentUid)}/jobs`,
|
|
424
|
+
method: "GET",
|
|
425
|
+
});
|
|
426
|
+
if (!res.ok) {
|
|
427
|
+
console.debug("Scheduled job preflight was unavailable; proceeding with job creation.", { status: res.status });
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const body = await res.json();
|
|
431
|
+
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
432
|
+
executionEnabled = body.executionEnabled;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
console.debug("Scheduled job preflight failed; proceeding with job creation.", error);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (executionEnabled === false) {
|
|
440
|
+
throw new ReportPublishError(JOB_EXECUTION_DISABLED_MESSAGE, "job-execution-disabled");
|
|
441
|
+
}
|
|
442
|
+
if (executionEnabled !== true) {
|
|
443
|
+
console.debug("Scheduled job preflight did not return a boolean executionEnabled; proceeding with job creation.");
|
|
444
|
+
}
|
|
445
|
+
}
|
|
417
446
|
async function adoptAction(reportId, options) {
|
|
418
447
|
const token = await ensureCognitoToken();
|
|
419
448
|
const report = await resolveReport(token, reportId, options.channel);
|
|
420
449
|
const updated = await adoptReport(report, {
|
|
450
|
+
preflightJobCreation: () => requireScheduledJobsEnabled(token, report.ownerAgentUid),
|
|
421
451
|
cancelJob: (jobId) => cancelOwnedJob(token, report.ownerAgentUid, jobId),
|
|
422
452
|
createJob: async (input) => {
|
|
423
453
|
const res = await vaultApiFetch({
|
|
@@ -426,8 +456,21 @@ async function adoptAction(reportId, options) {
|
|
|
426
456
|
method: "POST",
|
|
427
457
|
body: input,
|
|
428
458
|
});
|
|
429
|
-
if (!res.ok)
|
|
459
|
+
if (!res.ok) {
|
|
460
|
+
if (res.status === 409) {
|
|
461
|
+
let rejection;
|
|
462
|
+
try {
|
|
463
|
+
rejection = await res.json();
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
// A non-JSON conflict response is still the generic install failure.
|
|
467
|
+
}
|
|
468
|
+
if (rejection?.code === "JOB_EXECUTION_DISABLED") {
|
|
469
|
+
throw new ReportPublishError(JOB_EXECUTION_DISABLED_MESSAGE, "job-execution-disabled");
|
|
470
|
+
}
|
|
471
|
+
}
|
|
430
472
|
throw new ReportPublishError("Could not install the report job.");
|
|
473
|
+
}
|
|
431
474
|
const body = await res.json();
|
|
432
475
|
const jobId = body.job?.jobId ?? body.jobId;
|
|
433
476
|
if (!jobId)
|