@indigoai-us/hq-cli 5.142.0 → 5.144.0
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 +1 -1
- package/dist/command-catalog.generated.js +1 -1
- package/dist/commands/agents.d.ts +1 -0
- package/dist/commands/agents.js +4 -8
- package/dist/commands/dm.js +1 -5
- package/dist/commands/lanes.js +49 -6
- package/dist/commands/mesh.js +25 -2
- package/dist/commands/report.d.ts +3 -1
- package/dist/commands/report.js +69 -15
- package/dist/lib/agent-kit/mcp/tools.js +2 -1
- package/dist/lib/bot/api.d.ts +4 -0
- package/dist/lib/bot/api.js +13 -1
- package/dist/lib/lanes/models.d.ts +6 -0
- package/dist/lib/lanes/models.js +24 -0
- package/dist/lib/lanes/reconcile.js +19 -2
- package/dist/lib/lanes/spawn.d.ts +4 -1
- package/dist/lib/lanes/spawn.js +15 -0
- package/dist/lib/notify-recipient.d.ts +7 -0
- package/dist/lib/notify-recipient.js +11 -0
- package/dist/lib/report-publish.d.ts +2 -2
- package/dist/lib/report-publish.js +9 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.144.0] — 2026-09-23
|
|
6
|
+
|
|
7
|
+
- Worker-profile models now use the same provider catalog validation as
|
|
8
|
+
`--model` during creation and before every later provider launch. The error
|
|
9
|
+
names the profile when its model is unsupported.
|
|
10
|
+
- Worker resolution continues to use the existing personal, core, installed
|
|
11
|
+
package, and company worker sources. hq-cli does not ship starter profiles.
|
|
12
|
+
|
|
13
|
+
## [5.143.0] — 2026-09-23
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- `hq dm` and report commands now reject non-recipient and non-company channel
|
|
18
|
+
requests locally, and report updates retry one fresh revision after a
|
|
19
|
+
conflict.
|
|
20
|
+
- `hq mesh project register` now verifies the company's Board view before
|
|
21
|
+
registering and reports an actionable local error when the Board is missing.
|
|
22
|
+
|
|
5
23
|
## [5.142.0] — 2026-09-23
|
|
6
24
|
|
|
7
25
|
### Added
|
|
@@ -5740,7 +5740,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
5740
5740
|
readonly subcommands: readonly [];
|
|
5741
5741
|
}, {
|
|
5742
5742
|
readonly name: "register";
|
|
5743
|
-
readonly description: "
|
|
5743
|
+
readonly description: "Register an existing Board and bind its project channel; use project ensure to create or repair a Board";
|
|
5744
5744
|
readonly aliases: readonly [];
|
|
5745
5745
|
readonly hidden: false;
|
|
5746
5746
|
readonly usage: "[options] <projectId>";
|
|
@@ -7409,7 +7409,7 @@ export const COMMAND_CATALOG = [
|
|
|
7409
7409
|
},
|
|
7410
7410
|
{
|
|
7411
7411
|
"name": "register",
|
|
7412
|
-
"description": "
|
|
7412
|
+
"description": "Register an existing Board and bind its project channel; use project ensure to create or repair a Board",
|
|
7413
7413
|
"aliases": [],
|
|
7414
7414
|
"hidden": false,
|
|
7415
7415
|
"usage": "[options] <projectId>",
|
|
@@ -336,6 +336,7 @@ export declare function formatJobsTable(jobs: AgentJobView[]): string;
|
|
|
336
336
|
export declare function formatPauseResult(result: PauseJobResult): string;
|
|
337
337
|
/** Human-readable "hot-applied to the running box" vs "saved for next launch". */
|
|
338
338
|
export declare function appliedHint(applied: boolean): string;
|
|
339
|
+
/** Agent uid shape (`agt_…`). */
|
|
339
340
|
/** One message in a DM thread, as returned by `GET /v1/notify/thread`. */
|
|
340
341
|
export interface DmThreadMessage {
|
|
341
342
|
eventId: string;
|
package/dist/commands/agents.js
CHANGED
|
@@ -36,6 +36,7 @@ import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
|
36
36
|
import { launchSessionManagerPlugin, SessionManagerPluginLaunchError, TerminalSessionTimeoutError, waitForTerminalSession, } from "@indigoai-us/hq-cloud/outposts/node";
|
|
37
37
|
import { peekIdToken } from "../utils/id-token.js";
|
|
38
38
|
import { isPlanGateError } from "../utils/plan-gate-error.js";
|
|
39
|
+
import { AGENT_UID_PATTERN, isAgentUid } from "../lib/notify-recipient.js";
|
|
39
40
|
import { confirmChargeOrExit, formatUsd, parseBillingPayload, surfaceBillingBlocked, } from "../utils/billing-gate.js";
|
|
40
41
|
/** Reasoning-effort values hq-pro accepts on `runtime-config`. */
|
|
41
42
|
export const VALID_EFFORTS = new Set([
|
|
@@ -807,14 +808,6 @@ export function appliedHint(applied) {
|
|
|
807
808
|
? "applied to the running box now"
|
|
808
809
|
: "saved — takes effect on the agent's next launch";
|
|
809
810
|
}
|
|
810
|
-
// ---------------------------------------------------------------------------
|
|
811
|
-
// Talking to agents (DM). Agents are members addressable over HQ's DM system:
|
|
812
|
-
// a message rides `POST /v1/notify/dm` (the wire contract accepts `agt_*`), the
|
|
813
|
-
// agent's box picks it up and replies over DM (a first-class agent reply
|
|
814
|
-
// channel), and the two-way conversation is read back via `GET /v1/notify/thread`.
|
|
815
|
-
// ---------------------------------------------------------------------------
|
|
816
|
-
/** Agent uid shape (`agt_…`). */
|
|
817
|
-
const AGENT_UID_PATTERN = /^agt_[A-Za-z0-9_-]+$/;
|
|
818
811
|
/**
|
|
819
812
|
* Resolve an agent reference to its `agt_` uid. An `agt_…` value is used
|
|
820
813
|
* directly; anything else is treated as a slug or display name and resolved
|
|
@@ -909,6 +902,9 @@ async function resolveJobsTargetOrExit(ref, company) {
|
|
|
909
902
|
}
|
|
910
903
|
/** Send a DM to an agent. Returns nothing meaningful beyond success. */
|
|
911
904
|
export async function sendAgentDm(token, agentUid, message) {
|
|
905
|
+
if (!isAgentUid(agentUid)) {
|
|
906
|
+
throw new Error(`Invalid agent recipient '${agentUid}': expected an agt_ uid.`);
|
|
907
|
+
}
|
|
912
908
|
await agentsRequest({
|
|
913
909
|
token,
|
|
914
910
|
path: "/v1/notify/dm",
|
package/dist/commands/dm.js
CHANGED
|
@@ -4,11 +4,7 @@ import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
|
4
4
|
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
5
5
|
import { isMachineIdentity } from "@indigoai-us/hq-cloud";
|
|
6
6
|
import { peekIdToken } from "../utils/id-token.js";
|
|
7
|
-
|
|
8
|
-
// People (prs_) and agents (agt_) are both first-class DM participants; the
|
|
9
|
-
// server applies the same membership-overlap gate to either and delivers
|
|
10
|
-
// agt_* recipients into the agent's durable box inbox.
|
|
11
|
-
const RECIPIENT_UID_PATTERN = /^(prs|agt)_[A-Za-z0-9_-]+$/;
|
|
7
|
+
import { EMAIL_PATTERN, RECIPIENT_UID_PATTERN } from "../lib/notify-recipient.js";
|
|
12
8
|
/**
|
|
13
9
|
* Classify a recipient arg as an email or a person/agent uid. Mirrors the
|
|
14
10
|
* email/uid heuristic used by `hq members`. Returns null for neither.
|
package/dist/commands/lanes.js
CHANGED
|
@@ -18,7 +18,7 @@ import { vaultApiFetch } from "../utils/vault-api.js";
|
|
|
18
18
|
import { openMeshTransport } from "../lib/mesh/client.js";
|
|
19
19
|
import { createLaneMesh, emitLaneDecisionEvents, } from "../lib/lanes/mesh-events.js";
|
|
20
20
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
21
|
-
import { AccountResolutionError, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountMaxRunning, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, 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, parseLaneState, parseLivenessState, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, isLoggedOut, runReconcilePass, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasRecordedSessionId, providerSupportsSessionResume, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
21
|
+
import { AccountResolutionError, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountMaxRunning, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, 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, parseLaneState, parseLivenessState, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, isLoggedOut, runReconcilePass, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasRecordedSessionId, providerSupportsSessionResume, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
22
22
|
import { questionBodyAbsPath } from "../lib/lanes/questions.js";
|
|
23
23
|
import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
24
24
|
import { providerCompactAfterMs } from "../lib/lanes/config.js";
|
|
@@ -26,7 +26,7 @@ import { parseCliEffort, resolveLaneEffort } from "../lib/lanes/effort.js";
|
|
|
26
26
|
import { LanesError } from "../lib/lanes/errors.js";
|
|
27
27
|
import { accountQuotaView, loadQuotaState, quotaBlockFor } from "../lib/lanes/quota.js";
|
|
28
28
|
import { SESSION_MOVE_REASON, copySessionFiles, planSessionCopy } from "../lib/lanes/move-account.js";
|
|
29
|
-
import { defaultProbe, validateLaneModel, } from "../lib/lanes/models.js";
|
|
29
|
+
import { defaultProbe, validateProfileLaneModel, validateLaneModel, } from "../lib/lanes/models.js";
|
|
30
30
|
import { parseCliModel, refuseOutpostSelfTarget, resolveLaneModel, FOREGROUND_ONLY_RULE, STANDING_AUTHORIZATION, spawnLaneWorker, stopLaneProcesses, } from "../lib/lanes/spawn.js";
|
|
31
31
|
export { spawnLaneWorker };
|
|
32
32
|
export { questionBodyAbsPath };
|
|
@@ -1530,13 +1530,16 @@ async function runCreate(ctx, opts) {
|
|
|
1530
1530
|
env: ctx.env,
|
|
1531
1531
|
};
|
|
1532
1532
|
const validatedAccounts = new Set();
|
|
1533
|
-
|
|
1533
|
+
const modelToValidate = cliModel ?? profileModel;
|
|
1534
|
+
const profileModelToValidate = cliModel === undefined && profileModel !== undefined ? profile : undefined;
|
|
1535
|
+
if (modelToValidate !== undefined && !outpostId) {
|
|
1534
1536
|
const accounts = config.providers[provider].accounts;
|
|
1535
1537
|
const candidate = pinnedAccount ??
|
|
1536
1538
|
resolveAccount(provider, undefined, config, {
|
|
1537
1539
|
hqRoot,
|
|
1538
1540
|
existsSync: ctx.existsSync,
|
|
1539
1541
|
cliModel,
|
|
1542
|
+
model: modelToValidate,
|
|
1540
1543
|
...(profileAccounts !== undefined
|
|
1541
1544
|
? { allowedAccounts: profileAccounts }
|
|
1542
1545
|
: {}),
|
|
@@ -1562,7 +1565,12 @@ async function runCreate(ctx, opts) {
|
|
|
1562
1565
|
continue;
|
|
1563
1566
|
checkedAccounts.push(id);
|
|
1564
1567
|
try {
|
|
1565
|
-
|
|
1568
|
+
if (profileModelToValidate !== undefined) {
|
|
1569
|
+
validateProfileLaneModel(provider, modelToValidate, home, profileModelToValidate.id, modelDeps);
|
|
1570
|
+
}
|
|
1571
|
+
else {
|
|
1572
|
+
validateLaneModel(provider, modelToValidate, home, modelDeps);
|
|
1573
|
+
}
|
|
1566
1574
|
}
|
|
1567
1575
|
catch (err) {
|
|
1568
1576
|
if (!(isLanesError(err) && err.error === "model_unsupported"))
|
|
@@ -1570,7 +1578,7 @@ async function runCreate(ctx, opts) {
|
|
|
1570
1578
|
unsupported.push({ account: id, error: err });
|
|
1571
1579
|
continue;
|
|
1572
1580
|
}
|
|
1573
|
-
const block = quotaBlockFor(quota, provider, id,
|
|
1581
|
+
const block = quotaBlockFor(quota, provider, id, modelToValidate, now);
|
|
1574
1582
|
if (block.blocked) {
|
|
1575
1583
|
quotaBlocked.push({ account: id, block });
|
|
1576
1584
|
continue;
|
|
@@ -1579,7 +1587,7 @@ async function runCreate(ctx, opts) {
|
|
|
1579
1587
|
}
|
|
1580
1588
|
if (validatedAccounts.size === 0 && quotaBlocked.length > 0) {
|
|
1581
1589
|
const blocked = quotaBlocked[0];
|
|
1582
|
-
const model = blocked.block.model ??
|
|
1590
|
+
const model = blocked.block.model ?? modelToValidate;
|
|
1583
1591
|
const checkedSuffix = ` Checked accounts: ${checkedAccounts.join(", ")}`;
|
|
1584
1592
|
if (blocked.block.reason === "account_model_unavailable") {
|
|
1585
1593
|
throw new AccountResolutionError("account_model_unavailable", `Account ${JSON.stringify(blocked.account)} cannot run model ${model ?? "(no model)"}: ${blocked.block.marker ?? "needs credits"}${checkedSuffix}`, {
|
|
@@ -1623,6 +1631,7 @@ async function runCreate(ctx, opts) {
|
|
|
1623
1631
|
hqRoot,
|
|
1624
1632
|
existsSync: ctx.existsSync,
|
|
1625
1633
|
cliModel,
|
|
1634
|
+
model: cliModel ?? profileModel,
|
|
1626
1635
|
...(allowedAccounts !== undefined ? { allowedAccounts } : {}),
|
|
1627
1636
|
}).id;
|
|
1628
1637
|
}
|
|
@@ -1748,6 +1757,11 @@ async function runCreate(ctx, opts) {
|
|
|
1748
1757
|
if (admission.action === "start") {
|
|
1749
1758
|
started = spawnLaneWorker(hqRoot, lane, {
|
|
1750
1759
|
env: ctx.env,
|
|
1760
|
+
modelDeps: {
|
|
1761
|
+
readFileSync: ctx.readFileSync,
|
|
1762
|
+
probe: ctx.probeProvider,
|
|
1763
|
+
env: ctx.env,
|
|
1764
|
+
},
|
|
1751
1765
|
existsSync: ctx.existsSync,
|
|
1752
1766
|
spawnProcess: ctx.spawnProcess,
|
|
1753
1767
|
proveDetached: ctx.proveDetached,
|
|
@@ -2049,6 +2063,11 @@ async function runPickup(ctx, opts) {
|
|
|
2049
2063
|
try {
|
|
2050
2064
|
started = spawnLaneWorker(hqRoot, afterAccount, {
|
|
2051
2065
|
env: ctx.env,
|
|
2066
|
+
modelDeps: {
|
|
2067
|
+
readFileSync: ctx.readFileSync,
|
|
2068
|
+
probe: ctx.probeProvider,
|
|
2069
|
+
env: ctx.env,
|
|
2070
|
+
},
|
|
2052
2071
|
existsSync: ctx.existsSync,
|
|
2053
2072
|
spawnProcess: ctx.spawnProcess,
|
|
2054
2073
|
proveDetached: ctx.proveDetached,
|
|
@@ -2219,6 +2238,20 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
2219
2238
|
if (isLoggedOut(accountStatus(lane.provider, target, config, { existsSync: ctx.existsSync }))) {
|
|
2220
2239
|
throw new LanesError("account_not_logged_in", `Account ${target} for ${lane.provider} is not logged in (no credential file)`, { provider: lane.provider, account: target, lane_id: laneId });
|
|
2221
2240
|
}
|
|
2241
|
+
const profile = lane.worker_profile !== undefined
|
|
2242
|
+
? loadWorkerProfile(hqRoot, lane.worker_profile)
|
|
2243
|
+
: undefined;
|
|
2244
|
+
const profileDefaultsApply = profile !== undefined && profile.meta.provider === lane.provider;
|
|
2245
|
+
const pinnedModel = lane.model_source === undefined || lane.model_source === "cli"
|
|
2246
|
+
? lane.model
|
|
2247
|
+
: undefined;
|
|
2248
|
+
if (input.cliModel === undefined && profileDefaultsApply && pinnedModel === undefined) {
|
|
2249
|
+
validateProfileLaneModel(lane.provider, profile.meta.model, targetCfg.home, profile.id, {
|
|
2250
|
+
readFileSync: ctx.readFileSync,
|
|
2251
|
+
probe: ctx.probeProvider,
|
|
2252
|
+
env: ctx.env,
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2222
2255
|
// The model the moved run would use, resolved the way spawn resolves it:
|
|
2223
2256
|
// the operator's --model, else the lane's own, else the target account's
|
|
2224
2257
|
// default, else the provider's. The quota and validation checks use this
|
|
@@ -2383,6 +2416,11 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
2383
2416
|
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
2384
2417
|
resumed = spawnLaneWorker(hqRoot, lane, {
|
|
2385
2418
|
env: ctx.env,
|
|
2419
|
+
modelDeps: {
|
|
2420
|
+
readFileSync: ctx.readFileSync,
|
|
2421
|
+
probe: ctx.probeProvider,
|
|
2422
|
+
env: ctx.env,
|
|
2423
|
+
},
|
|
2386
2424
|
existsSync: ctx.existsSync,
|
|
2387
2425
|
spawnProcess: ctx.spawnProcess,
|
|
2388
2426
|
proveDetached: ctx.proveDetached,
|
|
@@ -2529,6 +2567,11 @@ async function runResume(ctx, laneId, opts) {
|
|
|
2529
2567
|
? lane
|
|
2530
2568
|
: spawnLaneWorker(hqRoot, lane, {
|
|
2531
2569
|
env: ctx.env,
|
|
2570
|
+
modelDeps: {
|
|
2571
|
+
readFileSync: ctx.readFileSync,
|
|
2572
|
+
probe: ctx.probeProvider,
|
|
2573
|
+
env: ctx.env,
|
|
2574
|
+
},
|
|
2532
2575
|
existsSync: ctx.existsSync,
|
|
2533
2576
|
spawnProcess: ctx.spawnProcess,
|
|
2534
2577
|
proveDetached: ctx.proveDetached,
|
package/dist/commands/mesh.js
CHANGED
|
@@ -400,7 +400,30 @@ async function runProjectRegister(projectId, opts) {
|
|
|
400
400
|
}
|
|
401
401
|
const { token, company } = await withCompany(opts);
|
|
402
402
|
const principalUid = await resolveMeshPrincipalUid(token, loadCachedTokens());
|
|
403
|
-
const
|
|
403
|
+
const companyRef = company.companySlug || company.companyUid;
|
|
404
|
+
try {
|
|
405
|
+
const board = await getProjectView(token, company.companyUid, projectId);
|
|
406
|
+
if (board.companyUid !== company.companyUid || board.projectId !== projectId) {
|
|
407
|
+
fail(`Work mesh project ${projectId} has no Board view in company ${companyRef}. Create the Board before registering it.`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
if (httpStatus(err) === 404) {
|
|
412
|
+
fail(`Work mesh project ${projectId} has no Board view in company ${companyRef}. Create the Board before registering it.`);
|
|
413
|
+
}
|
|
414
|
+
throw err;
|
|
415
|
+
}
|
|
416
|
+
let registration;
|
|
417
|
+
try {
|
|
418
|
+
registration = await registerMeshProject(token, company.companyUid, projectId);
|
|
419
|
+
}
|
|
420
|
+
catch (err) {
|
|
421
|
+
const failure = err;
|
|
422
|
+
if (failure.status === 403 && failure.code === "PROJECT_NOT_IN_COMPANY") {
|
|
423
|
+
fail(`Work mesh project ${projectId} could not be registered in company ${companyRef}: the Board or its genesis thread is missing. Run "hq mesh project ensure ${projectId} --company ${companyRef} --stories-file <prd.json>" to create or restore them.`);
|
|
424
|
+
}
|
|
425
|
+
throw err;
|
|
426
|
+
}
|
|
404
427
|
const boardState = "registered";
|
|
405
428
|
if (opts.json) {
|
|
406
429
|
console.log(JSON.stringify({ ok: true, action: "project.register", principalUid, boardState, ...registration }, null, 2));
|
|
@@ -1601,7 +1624,7 @@ export function registerMeshCommand(program) {
|
|
|
1601
1624
|
.action((projectId, opts) => wrap(() => runProjectEnsure(projectId, opts))());
|
|
1602
1625
|
project
|
|
1603
1626
|
.command("register <projectId>")
|
|
1604
|
-
.description("
|
|
1627
|
+
.description("Register an existing Board and bind its project channel; use project ensure to create or repair a Board")
|
|
1605
1628
|
.requiredOption("--company <slug|uid>", "Company slug or cloud uid")
|
|
1606
1629
|
.option("--channel <channelId>", "Reserved: the current server chooses the project channel")
|
|
1607
1630
|
.option("--json", "Print machine-readable JSON")
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { buildReportTarball, deployOrgContext, type ChannelReport, type DeployAccessPolicy, type ReportDeployment, type ReportPatch } from "../lib/report-publish.js";
|
|
3
|
+
type ReportPatchInput = ReportPatch | ((current: ChannelReport) => ReportPatch);
|
|
3
4
|
/** Explicit context has priority; discovery is the authenticated fallback for normal CLI use. */
|
|
4
5
|
export declare function resolveReport(token: string, reportId: string, requestedChannelId?: string): Promise<ChannelReport>;
|
|
5
|
-
export declare function patchReport(token: string, report: ChannelReport, patch:
|
|
6
|
+
export declare function patchReport(token: string, report: ChannelReport, patch: ReportPatchInput): Promise<ChannelReport>;
|
|
6
7
|
export declare function resolveCompanySlug(token: string, companyUid: string): Promise<string>;
|
|
7
8
|
export declare function deployReportStatic(token: string, input: {
|
|
8
9
|
slug: string;
|
|
@@ -12,4 +13,5 @@ export declare function deployReportStatic(token: string, input: {
|
|
|
12
13
|
}): Promise<ReportDeployment>;
|
|
13
14
|
export declare function registerReportCommand(program: Command): void;
|
|
14
15
|
export declare function runReportCommand(action: () => Promise<void>): Promise<void>;
|
|
16
|
+
export {};
|
|
15
17
|
//# sourceMappingURL=report.d.ts.map
|
package/dist/commands/report.js
CHANGED
|
@@ -86,10 +86,39 @@ function print(value, json) {
|
|
|
86
86
|
else
|
|
87
87
|
console.log(typeof value === "string" ? value : JSON.stringify(value, null, 2));
|
|
88
88
|
}
|
|
89
|
+
async function listReportChannels(token) {
|
|
90
|
+
const channels = await vaultApiFetch({ token, path: "/v1/notify/channels" });
|
|
91
|
+
if (!channels.ok)
|
|
92
|
+
throw new ReportPublishError("Could not resolve the report channel.");
|
|
93
|
+
const listed = await channels.json();
|
|
94
|
+
return Array.isArray(listed.channels) ? listed.channels : [];
|
|
95
|
+
}
|
|
96
|
+
function isCompanyReportChannel(channel) {
|
|
97
|
+
return channel.scope === "company"
|
|
98
|
+
|| (typeof channel.scope !== "string" && Boolean(channel.companyUid));
|
|
99
|
+
}
|
|
100
|
+
function requireCompanyReportChannel(channels, channelId) {
|
|
101
|
+
const channel = channels.find((row) => row.channelId === channelId);
|
|
102
|
+
if (!channel)
|
|
103
|
+
throw new ReportPublishError("Report channel was not found in your channels.");
|
|
104
|
+
if (!isCompanyReportChannel(channel))
|
|
105
|
+
throw new ReportPublishError("Channel reports require a company channel.");
|
|
106
|
+
}
|
|
107
|
+
async function readReportInChannel(token, channelId, reportId) {
|
|
108
|
+
const res = await vaultApiFetch({ token, path: reportPath(channelId, reportId) });
|
|
109
|
+
if (!res.ok)
|
|
110
|
+
return undefined;
|
|
111
|
+
const body = await res.json();
|
|
112
|
+
if (!body.report)
|
|
113
|
+
throw new ReportPublishError("Report response was incomplete.");
|
|
114
|
+
return body.report;
|
|
115
|
+
}
|
|
89
116
|
/** Explicit context has priority; discovery is the authenticated fallback for normal CLI use. */
|
|
90
117
|
export async function resolveReport(token, reportId, requestedChannelId) {
|
|
91
118
|
const explicit = requestedChannelId ?? process.env.HQ_REPORT_CHANNEL_ID;
|
|
119
|
+
const channels = await listReportChannels(token);
|
|
92
120
|
if (explicit) {
|
|
121
|
+
requireCompanyReportChannel(channels, explicit);
|
|
93
122
|
const res = await vaultApiFetch({ token, path: reportPath(explicit, reportId) });
|
|
94
123
|
if (!res.ok)
|
|
95
124
|
throw new ReportPublishError("Report was not found in the supplied channel context.");
|
|
@@ -100,24 +129,20 @@ export async function resolveReport(token, reportId, requestedChannelId) {
|
|
|
100
129
|
}
|
|
101
130
|
const recordedChannel = locallyRecordedChannel(reportId);
|
|
102
131
|
if (recordedChannel) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
return body.report;
|
|
108
|
-
}
|
|
132
|
+
requireCompanyReportChannel(channels, recordedChannel);
|
|
133
|
+
const recorded = await readReportInChannel(token, recordedChannel, reportId);
|
|
134
|
+
if (recorded)
|
|
135
|
+
return recorded;
|
|
109
136
|
}
|
|
110
137
|
// The report API is intentionally channel-scoped. Discover only among the
|
|
111
138
|
// caller's existing channels, using list endpoints so missing reports never
|
|
112
139
|
// become one server-side 404 per channel.
|
|
113
|
-
const channels = await vaultApiFetch({ token, path: "/v1/notify/channels" });
|
|
114
|
-
if (!channels.ok)
|
|
115
|
-
throw new ReportPublishError("Could not resolve the report channel.");
|
|
116
|
-
const listed = await channels.json();
|
|
117
140
|
let scannedChannels = 0;
|
|
118
|
-
for (const row of
|
|
141
|
+
for (const row of channels) {
|
|
119
142
|
if (!row.channelId)
|
|
120
143
|
continue;
|
|
144
|
+
if (!isCompanyReportChannel(row))
|
|
145
|
+
continue;
|
|
121
146
|
if (scannedChannels >= MAX_REPORT_DISCOVERY_CHANNELS)
|
|
122
147
|
break;
|
|
123
148
|
scannedChannels += 1;
|
|
@@ -125,19 +150,45 @@ export async function resolveReport(token, reportId, requestedChannelId) {
|
|
|
125
150
|
if (!res.ok)
|
|
126
151
|
continue;
|
|
127
152
|
const body = await res.json();
|
|
128
|
-
const
|
|
153
|
+
const reports = Array.isArray(body.reports) ? body.reports : [];
|
|
154
|
+
const match = reports.find((report) => report.reportId === reportId);
|
|
129
155
|
if (match)
|
|
130
156
|
return match;
|
|
131
157
|
}
|
|
132
158
|
throw new ReportPublishError(notFoundMessage(scannedChannels));
|
|
133
159
|
}
|
|
134
160
|
export async function patchReport(token, report, patch) {
|
|
135
|
-
|
|
161
|
+
if (!report.companyUid)
|
|
162
|
+
throw new ReportPublishError("Channel reports require a company channel.");
|
|
163
|
+
const patchOnce = (current) => vaultApiFetch({
|
|
164
|
+
token,
|
|
165
|
+
path: reportPath(current.channelId, current.reportId),
|
|
166
|
+
method: "PATCH",
|
|
167
|
+
body: {
|
|
168
|
+
...(typeof patch === "function" ? patch(current) : patch),
|
|
169
|
+
revision: current.revision,
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
let res = await patchOnce(report);
|
|
136
173
|
if (res.status === 409) {
|
|
137
174
|
const body = await res.json().catch(() => ({}));
|
|
138
175
|
if (body.error === "already-running")
|
|
139
176
|
throw new ReportPublishError("already running", "already-running");
|
|
140
|
-
|
|
177
|
+
if (body.error !== "revision-mismatch")
|
|
178
|
+
throw new ReportPublishError("Report changed while it was being updated.", "revision-conflict");
|
|
179
|
+
const latest = await readReportInChannel(token, report.channelId, report.reportId);
|
|
180
|
+
if (!latest)
|
|
181
|
+
throw new ReportPublishError("Could not reread the report after it changed.", "revision-conflict");
|
|
182
|
+
if (typeof patch !== "function") {
|
|
183
|
+
throw new ReportPublishError("Report changed while it was being updated; refresh and retry.", "revision-conflict");
|
|
184
|
+
}
|
|
185
|
+
res = await patchOnce(latest);
|
|
186
|
+
if (res.status === 409) {
|
|
187
|
+
const retryBody = await res.json().catch(() => ({}));
|
|
188
|
+
if (retryBody.error === "already-running")
|
|
189
|
+
throw new ReportPublishError("already running", "already-running");
|
|
190
|
+
throw new ReportPublishError("Report changed while it was being updated; refresh and retry.", "revision-conflict");
|
|
191
|
+
}
|
|
141
192
|
}
|
|
142
193
|
if (!res.ok)
|
|
143
194
|
throw new ReportPublishError("Could not update the report record.");
|
|
@@ -267,7 +318,10 @@ async function discardDraftAction(reportId, options) {
|
|
|
267
318
|
if (!res.ok && res.status !== 404)
|
|
268
319
|
throw new ReportPublishError("Could not remove the draft deployment.");
|
|
269
320
|
}
|
|
270
|
-
const updated = await patchReport(token, report,
|
|
321
|
+
const updated = await patchReport(token, report, (current) => ({
|
|
322
|
+
draft: null,
|
|
323
|
+
state: current.url ? "live" : "draft-pending",
|
|
324
|
+
}));
|
|
271
325
|
print(updated, options.json === true);
|
|
272
326
|
}
|
|
273
327
|
async function accessAction(reportId, options) {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { CLI_VERSION } from "../../../cli-version.js";
|
|
19
19
|
import { peekIdToken } from "../../../utils/id-token.js";
|
|
20
|
+
import { isRecipientUid } from "../../../lib/notify-recipient.js";
|
|
20
21
|
import { markInboxDone, mergeAgentInbox, readDoneIds, summarizeInboxEntry } from "../inbox-state.js";
|
|
21
22
|
import { errorResult, McpToolInputError, textResult } from "./jsonrpc.js";
|
|
22
23
|
export const MAX_TOOL_OUTPUT_CHARS = 40_000;
|
|
@@ -222,7 +223,7 @@ export function buildAgentMcpTools(clients) {
|
|
|
222
223
|
const message = str(args, "message", { required: true, max: 20_000 }).trim();
|
|
223
224
|
const body = /^[^\s]+@[^\s]+$/.test(to)
|
|
224
225
|
? { toEmail: to.toLowerCase(), body: message }
|
|
225
|
-
:
|
|
226
|
+
: isRecipientUid(to)
|
|
226
227
|
? { toPersonUid: to, body: message }
|
|
227
228
|
: (() => {
|
|
228
229
|
throw new McpToolInputError('"to" must be an email, prs_… or agt_… uid');
|
package/dist/lib/bot/api.d.ts
CHANGED
|
@@ -143,6 +143,10 @@ export declare class BotApiError extends Error {
|
|
|
143
143
|
readonly tokenAgentUid?: string | undefined;
|
|
144
144
|
constructor(status: number, path: string, code?: string | undefined, detail?: string | undefined, retryAfterMs?: number | undefined, tokenAgentUid?: string | undefined);
|
|
145
145
|
}
|
|
146
|
+
export declare class InvalidDmRecipientError extends Error {
|
|
147
|
+
readonly recipient: string;
|
|
148
|
+
constructor(recipient: string);
|
|
149
|
+
}
|
|
146
150
|
/** Parse a Retry-After header (delta-seconds or HTTP-date) to milliseconds. */
|
|
147
151
|
export declare function retryAfterHeaderMs(value: string | null | undefined, nowMs?: number): number | undefined;
|
|
148
152
|
export interface BotApiOptions {
|
package/dist/lib/bot/api.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { vaultApiFetch } from "../../utils/vault-api.js";
|
|
9
9
|
import { tokenAgentUid } from "../heartbeat-identity.js";
|
|
10
10
|
import { assertNoSecretKeys } from "./local-config.js";
|
|
11
|
+
import { isRecipientUid } from "../notify-recipient.js";
|
|
11
12
|
export class BotApiError extends Error {
|
|
12
13
|
status;
|
|
13
14
|
path;
|
|
@@ -26,6 +27,14 @@ export class BotApiError extends Error {
|
|
|
26
27
|
this.name = "BotApiError";
|
|
27
28
|
}
|
|
28
29
|
}
|
|
30
|
+
export class InvalidDmRecipientError extends Error {
|
|
31
|
+
recipient;
|
|
32
|
+
constructor(recipient) {
|
|
33
|
+
super(`Invalid DM recipient '${recipient}': expected a prs_ or agt_ uid.`);
|
|
34
|
+
this.recipient = recipient;
|
|
35
|
+
this.name = "InvalidDmRecipientError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
29
38
|
/** Parse a Retry-After header (delta-seconds or HTTP-date) to milliseconds. */
|
|
30
39
|
export function retryAfterHeaderMs(value, nowMs = Date.now()) {
|
|
31
40
|
if (!value)
|
|
@@ -141,7 +150,10 @@ export class BotApi {
|
|
|
141
150
|
}
|
|
142
151
|
/** POST /v1/notify/dm — send as the bot (agent identity). */
|
|
143
152
|
async sendDm(input) {
|
|
144
|
-
const
|
|
153
|
+
const recipient = input.toPersonUid.trim();
|
|
154
|
+
if (!isRecipientUid(recipient))
|
|
155
|
+
throw new InvalidDmRecipientError(input.toPersonUid);
|
|
156
|
+
const payload = { toPersonUid: recipient, body: input.body };
|
|
145
157
|
if (input.rootEventId)
|
|
146
158
|
payload.rootEventId = input.rootEventId;
|
|
147
159
|
const { body } = await this.call("/v1/notify/dm", { method: "POST", body: payload });
|
|
@@ -75,4 +75,10 @@ export declare function modelIsKnown(known: KnownModels, model: string): boolean
|
|
|
75
75
|
* null when no source was readable and the value passed unvalidated.
|
|
76
76
|
*/
|
|
77
77
|
export declare function validateLaneModel(provider: LaneProvider, model: string, home: string, deps?: KnownModelsDeps): string | null;
|
|
78
|
+
/**
|
|
79
|
+
* Validate a model supplied by a reusable worker profile. Keep the provider
|
|
80
|
+
* catalog and refusal semantics identical to `--model`, while making the
|
|
81
|
+
* profile origin visible to callers and JSON error consumers.
|
|
82
|
+
*/
|
|
83
|
+
export declare function validateProfileLaneModel(provider: LaneProvider, model: string, home: string, profileId: string, deps?: KnownModelsDeps): string | null;
|
|
78
84
|
//# sourceMappingURL=models.d.ts.map
|
package/dist/lib/lanes/models.js
CHANGED
|
@@ -202,4 +202,28 @@ export function validateLaneModel(provider, model, home, deps = {}) {
|
|
|
202
202
|
// `message` is in details too: LanesError.toJSON prints details only.
|
|
203
203
|
throw new LanesError("model_unsupported", message, { provider, model, accepted, source: known.source, message });
|
|
204
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Validate a model supplied by a reusable worker profile. Keep the provider
|
|
207
|
+
* catalog and refusal semantics identical to `--model`, while making the
|
|
208
|
+
* profile origin visible to callers and JSON error consumers.
|
|
209
|
+
*/
|
|
210
|
+
export function validateProfileLaneModel(provider, model, home, profileId, deps = {}) {
|
|
211
|
+
try {
|
|
212
|
+
return validateLaneModel(provider, model, home, deps);
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
if (!(err instanceof LanesError) || err.error !== "model_unsupported") {
|
|
216
|
+
throw err;
|
|
217
|
+
}
|
|
218
|
+
const message = `Worker profile ${JSON.stringify(profileId)} supplied model ${JSON.stringify(model)}; ` +
|
|
219
|
+
`the model came from the profile. ${err.message}`;
|
|
220
|
+
throw new LanesError("model_unsupported", message, {
|
|
221
|
+
...err.details,
|
|
222
|
+
profile_id: profileId,
|
|
223
|
+
worker_profile: profileId,
|
|
224
|
+
model_source: "profile",
|
|
225
|
+
message,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
205
229
|
//# sourceMappingURL=models.js.map
|
|
@@ -13,7 +13,7 @@ import { MAX_LANE_DEPTH } from "./constants.js";
|
|
|
13
13
|
import { LanesError, isLaneVersionConflict } from "./errors.js";
|
|
14
14
|
import { loadWorkerProfile } from "./profile.js";
|
|
15
15
|
import { classifySessionLiveness, defaultLivenessDeps, loadLivenessIndex, readDaemonPresenceConnected, } from "./liveness.js";
|
|
16
|
-
import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, isPidRecordAlive, laneRunDir, readPidRecord, readSpawnManifest, spawnEnvelopeRepair, spawnLaneWorker, } from "./spawn.js";
|
|
16
|
+
import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, isPidRecordAlive, laneRunDir, readPidRecord, readSpawnManifest, markSpawnFailure, spawnEnvelopeRepair, spawnLaneWorker, } from "./spawn.js";
|
|
17
17
|
import { appendCappedHistory, lanesStoreRoot, parseLaneRecord, parseQuestionRecord, readLane, readQuestion, updateLane, updateQuestion, } from "./store.js";
|
|
18
18
|
import { applyThrottleDecision, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, throttleStatePath, writeThrottleState, } from "./throttle.js";
|
|
19
19
|
import { applyDefaultQuotaHold, readQuotaSignal, recordAccountQuota } from "./quota.js";
|
|
@@ -60,6 +60,19 @@ function markDirty(summary, skip) {
|
|
|
60
60
|
summary.ok = false;
|
|
61
61
|
logSkip(skip);
|
|
62
62
|
}
|
|
63
|
+
function terminalizeModelUnsupported(hqRoot, lane, err) {
|
|
64
|
+
if (!(err instanceof LanesError) || err.error !== "model_unsupported")
|
|
65
|
+
return;
|
|
66
|
+
try {
|
|
67
|
+
markSpawnFailure(hqRoot, lane.lane_id, err.error, {
|
|
68
|
+
...err.details,
|
|
69
|
+
message: err.message,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch (writeErr) {
|
|
73
|
+
process.stderr.write(`hq lanes reconcile: failed to terminalize ${lane.lane_id} after model validation: ${writeErr instanceof Error ? writeErr.message : String(writeErr)}\n`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
63
76
|
function markError(summary, err) {
|
|
64
77
|
const message = err instanceof Error ? err.message : String(err);
|
|
65
78
|
const code = err instanceof LanesError ? err.error : undefined;
|
|
@@ -843,7 +856,9 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
|
|
|
843
856
|
!quota &&
|
|
844
857
|
rawLatest &&
|
|
845
858
|
rawLatest.envelope === null) {
|
|
846
|
-
const
|
|
859
|
+
const spawnRepair = deps.spawnRepair ??
|
|
860
|
+
((root, repairLane, input) => spawnEnvelopeRepair(root, repairLane, input, deps.spawnLaneDeps));
|
|
861
|
+
const repair = decideEnvelopeRepair(hqRoot, lane.lane_id, rawLatest, nowMs, spawnRepair, () => undefined);
|
|
847
862
|
if (repair.action === "spawned") {
|
|
848
863
|
armLaneWatcher(hqRoot, lane.lane_id, deps.spawnLaneDeps);
|
|
849
864
|
summary.heartbeats.push(`${lane.lane_id}:envelope-repair`);
|
|
@@ -1067,6 +1082,7 @@ async function admitQueued(hqRoot, lanes, summary, deps, now) {
|
|
|
1067
1082
|
break;
|
|
1068
1083
|
}
|
|
1069
1084
|
catch (err) {
|
|
1085
|
+
terminalizeModelUnsupported(hqRoot, lane, err);
|
|
1070
1086
|
markDirty(summary, {
|
|
1071
1087
|
kind: "lane",
|
|
1072
1088
|
id: lane.lane_id,
|
|
@@ -1110,6 +1126,7 @@ async function resumeAwaiting(hqRoot, lanes, questions, summary, deps) {
|
|
|
1110
1126
|
});
|
|
1111
1127
|
}
|
|
1112
1128
|
catch (err) {
|
|
1129
|
+
terminalizeModelUnsupported(hqRoot, lane, err);
|
|
1113
1130
|
markDirty(summary, {
|
|
1114
1131
|
kind: "lane",
|
|
1115
1132
|
id: lane.lane_id,
|
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { PROVIDER_HOME_ENV } from "./accounts.js";
|
|
11
11
|
import { type RunCompactionFn } from "./compaction.js";
|
|
12
|
+
import { type KnownModelsDeps } from "./models.js";
|
|
12
13
|
import { type LaunchTrigger } from "./delivery.js";
|
|
13
14
|
import { type MachineIdentity } from "./machine-identity.js";
|
|
14
15
|
import { type LaneCompanyRef, type LaneProvider, type LaneRecord } from "./types.js";
|
|
15
16
|
export declare const LANES_RUNS_REL: string;
|
|
16
17
|
/** Written by reconcile before a resume spawn; the worker prompt names this path. */
|
|
17
18
|
export declare const ANSWERED_QUESTIONS_FILE = "answered-questions.json";
|
|
18
|
-
export declare const SPAWN_TERMINAL_REASONS: readonly ["account_not_logged_in", "unknown_account", "account_disabled", "no_enabled_account", "spawn_failed", "not_detached", "watcher_not_armed"];
|
|
19
|
+
export declare const SPAWN_TERMINAL_REASONS: readonly ["account_not_logged_in", "unknown_account", "account_disabled", "no_enabled_account", "spawn_failed", "not_detached", "watcher_not_armed", "model_unsupported"];
|
|
19
20
|
export type SpawnTerminalReason = (typeof SPAWN_TERMINAL_REASONS)[number];
|
|
20
21
|
declare const PROVIDER_COMMAND: Record<LaneProvider, string>;
|
|
21
22
|
export declare function providerCommandOnPath(command: string, pathEnv: string | undefined): boolean;
|
|
@@ -62,6 +63,8 @@ export type ProveDetachedFn = (pid: number) => DetachProof;
|
|
|
62
63
|
export type KillProcessFn = (pid: number, signal?: NodeJS.Signals) => void;
|
|
63
64
|
export interface SpawnLaneDeps {
|
|
64
65
|
env?: NodeJS.ProcessEnv;
|
|
66
|
+
/** Provider model catalog dependencies, shared with create in tests. */
|
|
67
|
+
modelDeps?: KnownModelsDeps;
|
|
65
68
|
existsSync?: (p: string) => boolean;
|
|
66
69
|
spawnProcess?: SpawnProcessFn;
|
|
67
70
|
proveDetached?: ProveDetachedFn;
|
package/dist/lib/lanes/spawn.js
CHANGED
|
@@ -16,6 +16,7 @@ import { COMPACTION_PROMPT, compactionAfterUsage, compactionDue, compactLaneSess
|
|
|
16
16
|
import { installDrainHook } from "./hooks-install.js";
|
|
17
17
|
import { loadLanesConfig, providerAutoCompactTokens, providerCompactAfterMs, } from "./config.js";
|
|
18
18
|
import { appendEffortFlag, resolveLaneEffort } from "./effort.js";
|
|
19
|
+
import { validateProfileLaneModel, } from "./models.js";
|
|
19
20
|
import { claimableStatesFor, planLaneLaunch, providerSupportsSessionResume, } from "./delivery.js";
|
|
20
21
|
import { AccountResolutionError, LanesError } from "./errors.js";
|
|
21
22
|
import { ignoredWorkerProfileFields, loadWorkerProfile, materializeWorkerProfile, workerProfilePrompt, } from "./profile.js";
|
|
@@ -33,6 +34,7 @@ export const SPAWN_TERMINAL_REASONS = [
|
|
|
33
34
|
"spawn_failed",
|
|
34
35
|
"not_detached",
|
|
35
36
|
"watcher_not_armed",
|
|
37
|
+
"model_unsupported",
|
|
36
38
|
];
|
|
37
39
|
const PROVIDER_COMMAND = {
|
|
38
40
|
claude: "claude",
|
|
@@ -1065,6 +1067,10 @@ function inferTrigger(lane) {
|
|
|
1065
1067
|
*/
|
|
1066
1068
|
export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
1067
1069
|
const env = deps.env ?? process.env;
|
|
1070
|
+
const modelDeps = {
|
|
1071
|
+
...deps.modelDeps,
|
|
1072
|
+
env: deps.modelDeps?.env ?? env,
|
|
1073
|
+
};
|
|
1068
1074
|
const existsSync = deps.existsSync ?? ((p) => fs.existsSync(p));
|
|
1069
1075
|
const spawnProcess = deps.spawnProcess ?? defaultSpawnProcess;
|
|
1070
1076
|
const proveDetached = deps.proveDetached ?? defaultProveDetached;
|
|
@@ -1150,6 +1156,9 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1150
1156
|
: existing;
|
|
1151
1157
|
throw new LanesError("account_not_logged_in", `Account ${resolved.id} for ${lane.provider} is not logged in (no credential file)`, { provider: lane.provider, account: resolved.id, lane_id: lane.lane_id, lane: failed });
|
|
1152
1158
|
}
|
|
1159
|
+
if (profileDefaultsApply && profileModel !== undefined && pinnedModel === undefined) {
|
|
1160
|
+
validateProfileLaneModel(existing.provider, profileModel, resolved.home, profile.id, modelDeps);
|
|
1161
|
+
}
|
|
1153
1162
|
if (lane.account !== resolved.id) {
|
|
1154
1163
|
retryUpdateLane(hqRoot, lane.lane_id, "spawn", (current) => {
|
|
1155
1164
|
if (current.account === resolved.id)
|
|
@@ -1532,6 +1541,12 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
|
|
|
1532
1541
|
existsSync,
|
|
1533
1542
|
model: pinnedModel ?? profileModel,
|
|
1534
1543
|
});
|
|
1544
|
+
if (profileDefaultsApply && profileModel !== undefined && pinnedModel === undefined) {
|
|
1545
|
+
validateProfileLaneModel(lane.provider, profileModel, resolved.home, profile.id, {
|
|
1546
|
+
...deps.modelDeps,
|
|
1547
|
+
env: deps.modelDeps?.env ?? env,
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1535
1550
|
const autoCompactTokens = lane.provider === "claude"
|
|
1536
1551
|
? providerAutoCompactTokens(config.providers.claude)
|
|
1537
1552
|
: undefined;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Canonical recipients accepted by the notify DM wire contract. */
|
|
2
|
+
export declare const RECIPIENT_UID_PATTERN: RegExp;
|
|
3
|
+
export declare const AGENT_UID_PATTERN: RegExp;
|
|
4
|
+
export declare const EMAIL_PATTERN: RegExp;
|
|
5
|
+
export declare function isRecipientUid(value: string): boolean;
|
|
6
|
+
export declare function isAgentUid(value: string): boolean;
|
|
7
|
+
//# sourceMappingURL=notify-recipient.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Canonical recipients accepted by the notify DM wire contract. */
|
|
2
|
+
export const RECIPIENT_UID_PATTERN = /^(prs|agt)_[A-Za-z0-9_-]+$/;
|
|
3
|
+
export const AGENT_UID_PATTERN = /^agt_[A-Za-z0-9_-]+$/;
|
|
4
|
+
export const EMAIL_PATTERN = /^[^\s]+@[^\s]+$/;
|
|
5
|
+
export function isRecipientUid(value) {
|
|
6
|
+
return RECIPIENT_UID_PATTERN.test(value.trim());
|
|
7
|
+
}
|
|
8
|
+
export function isAgentUid(value) {
|
|
9
|
+
return AGENT_UID_PATTERN.test(value.trim());
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=notify-recipient.js.map
|
|
@@ -124,8 +124,8 @@ export interface ReportAdoptDependencies {
|
|
|
124
124
|
patch(report: ChannelReport, patch: ReportPatch): Promise<ChannelReport>;
|
|
125
125
|
}
|
|
126
126
|
export declare class ReportPublishError extends Error {
|
|
127
|
-
readonly code: "already-running" | "source-invalid" | "deploy-failed" | "job-failed";
|
|
128
|
-
constructor(message: string, code?: "already-running" | "source-invalid" | "deploy-failed" | "job-failed");
|
|
127
|
+
readonly code: "already-running" | "source-invalid" | "deploy-failed" | "job-failed" | "revision-conflict";
|
|
128
|
+
constructor(message: string, code?: "already-running" | "source-invalid" | "deploy-failed" | "job-failed" | "revision-conflict");
|
|
129
129
|
}
|
|
130
130
|
/** A deliberately boring, token-safe failure reason stored on the report. */
|
|
131
131
|
export declare function plainFailureReason(error: unknown): string;
|
|
@@ -41,7 +41,7 @@ export class ReportPublishError extends Error {
|
|
|
41
41
|
/** A deliberately boring, token-safe failure reason stored on the report. */
|
|
42
42
|
export function plainFailureReason(error) {
|
|
43
43
|
if (error instanceof ReportPublishError) {
|
|
44
|
-
if (error.code === "source-invalid" || error.code === "job-failed")
|
|
44
|
+
if (error.code === "source-invalid" || error.code === "job-failed" || error.code === "revision-conflict")
|
|
45
45
|
return error.message.slice(0, 240);
|
|
46
46
|
if (error.code === "already-running")
|
|
47
47
|
return "already running";
|
|
@@ -264,9 +264,10 @@ export async function adoptReport(report, deps) {
|
|
|
264
264
|
export async function publishReport(report, directory, draft, deps) {
|
|
265
265
|
const startedAt = deps.now();
|
|
266
266
|
let locked = false;
|
|
267
|
+
let current = report;
|
|
267
268
|
try {
|
|
268
269
|
try {
|
|
269
|
-
await deps.patch(
|
|
270
|
+
current = await deps.patch(current, { lastRun: { at: startedAt, status: "running" } });
|
|
270
271
|
locked = true;
|
|
271
272
|
}
|
|
272
273
|
catch (error) {
|
|
@@ -276,9 +277,9 @@ export async function publishReport(report, directory, draft, deps) {
|
|
|
276
277
|
}
|
|
277
278
|
const source = loadReportSource(directory);
|
|
278
279
|
const deployment = await deps.deploy({
|
|
279
|
-
slug: reportDeploySlug(
|
|
280
|
+
slug: reportDeploySlug(current, draft),
|
|
280
281
|
files: source.files,
|
|
281
|
-
access: defaultAccessPolicy(
|
|
282
|
+
access: defaultAccessPolicy(current),
|
|
282
283
|
});
|
|
283
284
|
const completedAt = deps.now();
|
|
284
285
|
const patch = {
|
|
@@ -295,13 +296,13 @@ export async function publishReport(report, directory, draft, deps) {
|
|
|
295
296
|
patch.url = deployment.url;
|
|
296
297
|
patch.deployAppId = deployment.appId;
|
|
297
298
|
}
|
|
298
|
-
return await deps.patch(
|
|
299
|
+
return await deps.patch(current, patch);
|
|
299
300
|
}
|
|
300
301
|
catch (error) {
|
|
301
302
|
if (locked) {
|
|
302
303
|
const reason = plainFailureReason(error);
|
|
303
304
|
try {
|
|
304
|
-
await deps.patch(
|
|
305
|
+
await deps.patch(current, {
|
|
305
306
|
lastRun: { at: deps.now(), status: "failed", reason },
|
|
306
307
|
});
|
|
307
308
|
}
|
|
@@ -312,8 +313,8 @@ export async function publishReport(report, directory, draft, deps) {
|
|
|
312
313
|
const notice = ownerFailureNotice({
|
|
313
314
|
code: error instanceof ReportPublishError ? error.code : undefined,
|
|
314
315
|
reason,
|
|
315
|
-
reportUrl:
|
|
316
|
-
ownerPersonUid:
|
|
316
|
+
reportUrl: current.url,
|
|
317
|
+
ownerPersonUid: current.createdBy,
|
|
317
318
|
});
|
|
318
319
|
if (notice && deps.notifyOwner) {
|
|
319
320
|
try {
|