@indigoai-us/hq-cli 5.154.0 → 5.156.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 +48 -0
- package/dist/command-catalog.generated.d.ts +4 -1
- package/dist/command-catalog.generated.js +5 -1
- package/dist/commands/lanes.d.ts +3 -11
- package/dist/commands/lanes.js +162 -55
- package/dist/commands/mesh.js +35 -8
- package/dist/lib/lanes/accounts.d.ts +16 -11
- package/dist/lib/lanes/accounts.js +50 -8
- package/dist/lib/lanes/admission-host.d.ts +19 -0
- package/dist/lib/lanes/admission-host.js +33 -0
- package/dist/lib/lanes/config.js +25 -0
- package/dist/lib/lanes/mesh-events.d.ts +7 -3
- package/dist/lib/lanes/mesh-events.js +116 -20
- package/dist/lib/lanes/provider-rejection.d.ts +4 -4
- package/dist/lib/lanes/provider-rejection.js +24 -6
- package/dist/lib/lanes/reconcile.js +85 -2
- package/dist/lib/lanes/spawn.d.ts +9 -1
- package/dist/lib/lanes/spawn.js +420 -14
- package/dist/lib/lanes/types.d.ts +7 -3
- package/dist/lib/lanes/types.js +3 -2
- package/dist/lib/lanes/watcher.d.ts +8 -1
- package/dist/lib/lanes/watcher.js +66 -17
- package/dist/lib/mesh/api.d.ts +27 -1
- package/dist/lib/mesh/api.js +41 -12
- package/dist/lib/mesh/project-board.d.ts +1 -0
- package/dist/lib/mesh/project-board.js +1 -1
- package/package.json +1 -1
package/dist/commands/mesh.js
CHANGED
|
@@ -59,14 +59,22 @@ export function formatCheckLines(threads, company, projectId) {
|
|
|
59
59
|
const scope = projectId
|
|
60
60
|
? `${company.companySlug || company.companyUid}/${projectId}`
|
|
61
61
|
: company.companySlug || company.companyUid;
|
|
62
|
-
const lines = [`Work mesh: ${threads.length}
|
|
62
|
+
const lines = [`Work mesh: ${threads.length} thread(s) for ${scope}`];
|
|
63
63
|
for (const thread of threads.slice(0, 8)) {
|
|
64
64
|
const owner = thread.ownerUid ? ` owner=${thread.ownerUid}` : "";
|
|
65
65
|
const summary = thread.progressSummary ||
|
|
66
66
|
thread.sourceSignalSummary ||
|
|
67
67
|
thread.blockedReason ||
|
|
68
68
|
"";
|
|
69
|
-
|
|
69
|
+
const lane = thread.kind === "lane" ? thread.lane : undefined;
|
|
70
|
+
const laneDetails = lane
|
|
71
|
+
? ` lane=${lane.laneId} laneStatus=${thread.laneStatus ?? "unknown"}` +
|
|
72
|
+
` story=${lane.storyId ?? "unknown"} profile=${lane.workerProfile ?? "unknown"}` +
|
|
73
|
+
` model=${lane.model ?? "unknown"} account=${lane.account ?? "unknown"}` +
|
|
74
|
+
`${lane.parentLaneId ? ` parent=${lane.parentLaneId}` : ""}`
|
|
75
|
+
: "";
|
|
76
|
+
const machine = thread.senderMachineUid ? ` machine=${thread.senderMachineUid}` : "";
|
|
77
|
+
lines.push(`- ${thread.threadStatus ?? "unknown"} ${thread.threadId ?? "?"}${owner}${machine}${laneDetails}${summary ? `: ${summary}` : ""}`);
|
|
70
78
|
}
|
|
71
79
|
return lines;
|
|
72
80
|
}
|
|
@@ -320,6 +328,9 @@ function mergeProjectRepos(current, planned) {
|
|
|
320
328
|
return [...planned, ...current.filter((repo) => !plannedPaths.has(repo.path))];
|
|
321
329
|
}
|
|
322
330
|
async function runProjectSet(projectId, opts) {
|
|
331
|
+
if (opts.channel !== undefined && !opts.channel.trim()) {
|
|
332
|
+
fail("PROJECT_CHANNEL_INVALID: --channel must be a non-empty channel id.");
|
|
333
|
+
}
|
|
323
334
|
if (opts.storiesFile && opts.storiesJson) {
|
|
324
335
|
fail("Pass only one of --stories-file or --stories-json.");
|
|
325
336
|
}
|
|
@@ -358,6 +369,7 @@ async function runProjectSet(projectId, opts) {
|
|
|
358
369
|
const principalUid = await resolveMeshPrincipalUid(token, loadCachedTokens());
|
|
359
370
|
const PROJECT_CHANGED = "The Board changed while you were editing. Re-run to pick up the latest.";
|
|
360
371
|
let view;
|
|
372
|
+
let registration;
|
|
361
373
|
if (!current) {
|
|
362
374
|
const ensured = await ensureProjectBoard({
|
|
363
375
|
token,
|
|
@@ -367,10 +379,12 @@ async function runProjectSet(projectId, opts) {
|
|
|
367
379
|
description: opts.description !== undefined ? opts.description : source?.description ?? "",
|
|
368
380
|
stories: source?.stories ?? [],
|
|
369
381
|
repos: explicitRepos ?? source?.repos ?? [],
|
|
382
|
+
channelId: opts.channel,
|
|
370
383
|
});
|
|
371
384
|
view = ensured.view;
|
|
385
|
+
registration = ensured.registration;
|
|
372
386
|
}
|
|
373
|
-
else {
|
|
387
|
+
else if (opts.name !== undefined || opts.description !== undefined || source !== undefined || explicitRepos !== undefined) {
|
|
374
388
|
try {
|
|
375
389
|
view = await putProjectView(token, company.companyUid, projectId, {
|
|
376
390
|
name: opts.name?.trim() || source?.name || current.name,
|
|
@@ -395,18 +409,30 @@ async function runProjectSet(projectId, opts) {
|
|
|
395
409
|
throw err;
|
|
396
410
|
}
|
|
397
411
|
}
|
|
412
|
+
else {
|
|
413
|
+
view = current;
|
|
414
|
+
}
|
|
415
|
+
if (current && opts.channel !== undefined) {
|
|
416
|
+
registration = await registerMeshProject(token, company.companyUid, projectId, opts.channel);
|
|
417
|
+
}
|
|
398
418
|
if (!current && opts.create) {
|
|
399
419
|
noteCallingSessionBound(view.projectId, company);
|
|
400
420
|
}
|
|
401
421
|
if (opts.json) {
|
|
402
|
-
console.log(JSON.stringify({
|
|
422
|
+
console.log(JSON.stringify({
|
|
423
|
+
ok: true,
|
|
424
|
+
action: "project.set",
|
|
425
|
+
principalUid,
|
|
426
|
+
...view,
|
|
427
|
+
...(registration ? { registration } : {}),
|
|
428
|
+
}, null, 2));
|
|
403
429
|
return;
|
|
404
430
|
}
|
|
405
431
|
console.log(`Work mesh: project ${view.projectId} set (${view.name}; ${view.stories.length} stories)`);
|
|
406
432
|
}
|
|
407
433
|
async function runProjectRegister(projectId, opts) {
|
|
408
|
-
if (opts.channel
|
|
409
|
-
fail("--channel
|
|
434
|
+
if (opts.channel !== undefined && !opts.channel.trim()) {
|
|
435
|
+
fail("PROJECT_CHANNEL_INVALID: --channel must be a non-empty channel id.");
|
|
410
436
|
}
|
|
411
437
|
const { token, company } = await withCompany(opts);
|
|
412
438
|
const principalUid = await resolveMeshPrincipalUid(token, loadCachedTokens());
|
|
@@ -425,7 +451,7 @@ async function runProjectRegister(projectId, opts) {
|
|
|
425
451
|
}
|
|
426
452
|
let registration;
|
|
427
453
|
try {
|
|
428
|
-
registration = await registerMeshProject(token, company.companyUid, projectId);
|
|
454
|
+
registration = await registerMeshProject(token, company.companyUid, projectId, opts.channel);
|
|
429
455
|
}
|
|
430
456
|
catch (err) {
|
|
431
457
|
const failure = err;
|
|
@@ -1787,6 +1813,7 @@ export function registerMeshCommand(program) {
|
|
|
1787
1813
|
.option("--stories-json <json>", "Story array or JSON object with userStories")
|
|
1788
1814
|
.option("--repos-json <json>", "Repository array, such as [{path,branch}]")
|
|
1789
1815
|
.option("--repo <path[@branch]>", "Repository binding (repeatable)", collectRepeatable)
|
|
1816
|
+
.option("--channel <channelId>", "Bind the project to an existing company or project channel")
|
|
1790
1817
|
.option("--create", "Create the Board view when the GET is a 404")
|
|
1791
1818
|
.option("--json", "Print machine-readable JSON")
|
|
1792
1819
|
.action((projectId, opts) => wrap(() => runProjectSet(projectId, opts))());
|
|
@@ -1801,7 +1828,7 @@ export function registerMeshCommand(program) {
|
|
|
1801
1828
|
.command("register <projectId>")
|
|
1802
1829
|
.description("Register an existing Board and bind its project channel; use project ensure to create or repair a Board")
|
|
1803
1830
|
.requiredOption("--company <slug|uid>", "Company slug or cloud uid")
|
|
1804
|
-
.option("--channel <channelId>", "
|
|
1831
|
+
.option("--channel <channelId>", "Bind the project to an existing company or project channel")
|
|
1805
1832
|
.option("--json", "Print machine-readable JSON")
|
|
1806
1833
|
.action((projectId, opts) => wrap(() => runProjectRegister(projectId, opts))());
|
|
1807
1834
|
mesh
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Naming an account and omitting one mean different things: an explicit
|
|
5
5
|
* disabled id is refused; omitting picks the least-loaded eligible account.
|
|
6
6
|
* Status reports `configured` and `logged_in` separately. logged_in is
|
|
7
|
-
* derived from
|
|
8
|
-
* never reads, prints, copies, or hashes
|
|
7
|
+
* derived from credential-file existence or a provider status command; this
|
|
8
|
+
* module never reads, prints, copies, or hashes credential files.
|
|
9
9
|
*/
|
|
10
10
|
import { type LaneRecord, type LaneProvider } from "./types.js";
|
|
11
11
|
import type { LanesConfig } from "./types.js";
|
|
@@ -19,13 +19,22 @@ export declare const CLAUDE_CREDENTIALS_NAME = ".credentials.json";
|
|
|
19
19
|
* file here at all.
|
|
20
20
|
*/
|
|
21
21
|
export declare const PROVIDER_CREDENTIAL_FILES: Record<LaneProvider, readonly string[]>;
|
|
22
|
-
export type AuthState = "file" | "absent" | "unknown";
|
|
22
|
+
export type AuthState = "file" | "status" | "absent" | "unknown";
|
|
23
|
+
export interface AccountStatusProbeResult {
|
|
24
|
+
status: number | null;
|
|
25
|
+
stdout: string;
|
|
26
|
+
stderr: string;
|
|
27
|
+
}
|
|
23
28
|
export interface AccountDeps {
|
|
24
29
|
existsSync: (p: string) => boolean;
|
|
30
|
+
/** Provider status output is parsed for a boolean only; it is never printed. */
|
|
31
|
+
probe?: (command: string, args: string[], env: NodeJS.ProcessEnv) => AccountStatusProbeResult;
|
|
32
|
+
env?: NodeJS.ProcessEnv;
|
|
25
33
|
}
|
|
26
34
|
export interface AccountResolveContext {
|
|
27
35
|
hqRoot?: string;
|
|
28
36
|
existsSync?: (p: string) => boolean;
|
|
37
|
+
accountDeps?: Omit<AccountDeps, "existsSync">;
|
|
29
38
|
loadLanes?: (hqRoot: string) => LaneRecord[];
|
|
30
39
|
/** Already-resolved model for this lane. Beats cliModel and account defaults. */
|
|
31
40
|
model?: string;
|
|
@@ -38,6 +47,8 @@ export interface AccountResolveContext {
|
|
|
38
47
|
* set is never selected, so no refusal has to be decided under the lock.
|
|
39
48
|
*/
|
|
40
49
|
allowedAccounts?: readonly string[];
|
|
50
|
+
/** Ordered preferences; callers pass these only for a new lane's first assignment. */
|
|
51
|
+
preferredAccounts?: readonly string[];
|
|
41
52
|
}
|
|
42
53
|
export interface ResolvedAccount {
|
|
43
54
|
provider: LaneProvider;
|
|
@@ -54,13 +65,7 @@ export interface AccountStatus {
|
|
|
54
65
|
disabled_reason?: string;
|
|
55
66
|
/** Present in personal/lanes/config.json. */
|
|
56
67
|
configured: boolean;
|
|
57
|
-
/**
|
|
58
|
-
* True when a provider credential file exists. False only when every
|
|
59
|
-
* candidate is absent AND absence is conclusive for that provider
|
|
60
|
-
* (Codex, Grok). Null for Claude when no candidate exists: Claude can
|
|
61
|
-
* authenticate via keychain, so a missing file is not logged-out.
|
|
62
|
-
* Never inspects file contents.
|
|
63
|
-
*/
|
|
68
|
+
/** True/false comes from file existence or the provider status command. */
|
|
64
69
|
logged_in: boolean | null;
|
|
65
70
|
auth_state: AuthState;
|
|
66
71
|
envVar: string;
|
|
@@ -79,7 +84,7 @@ export declare function authStateFor(provider: LaneProvider, present: boolean):
|
|
|
79
84
|
export declare function loggedInFromAuthState(state: AuthState): boolean | null;
|
|
80
85
|
/** Admission and listing share this: refuse only a conclusive logged-out. */
|
|
81
86
|
export declare function isLoggedOut(status: Pick<AccountStatus, "logged_in">): boolean;
|
|
82
|
-
/** Auto-select requires a
|
|
87
|
+
/** Auto-select requires a positive file or provider-status login result. */
|
|
83
88
|
export declare function isCredentialProven(status: Pick<AccountStatus, "logged_in">): boolean;
|
|
84
89
|
/**
|
|
85
90
|
* Occupying states hold a slot: running / awaiting_input / blocked have a
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Naming an account and omitting one mean different things: an explicit
|
|
5
5
|
* disabled id is refused; omitting picks the least-loaded eligible account.
|
|
6
6
|
* Status reports `configured` and `logged_in` separately. logged_in is
|
|
7
|
-
* derived from
|
|
8
|
-
* never reads, prints, copies, or hashes
|
|
7
|
+
* derived from credential-file existence or a provider status command; this
|
|
8
|
+
* module never reads, prints, copies, or hashes credential files.
|
|
9
9
|
*/
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import * as path from "node:path";
|
|
@@ -76,7 +76,7 @@ export function loggedInFromAuthState(state) {
|
|
|
76
76
|
export function isLoggedOut(status) {
|
|
77
77
|
return status.logged_in === false;
|
|
78
78
|
}
|
|
79
|
-
/** Auto-select requires a
|
|
79
|
+
/** Auto-select requires a positive file or provider-status login result. */
|
|
80
80
|
export function isCredentialProven(status) {
|
|
81
81
|
return status.logged_in === true;
|
|
82
82
|
}
|
|
@@ -205,8 +205,12 @@ export function selectBalancedAccount(provider, config, ctx = {}) {
|
|
|
205
205
|
}
|
|
206
206
|
const proven = [];
|
|
207
207
|
for (const id of enabled) {
|
|
208
|
-
const
|
|
209
|
-
|
|
208
|
+
const status = accountStatus(provider, id, config, {
|
|
209
|
+
existsSync,
|
|
210
|
+
...(ctx.accountDeps?.probe ? { probe: ctx.accountDeps.probe } : {}),
|
|
211
|
+
...(ctx.accountDeps?.env ? { env: ctx.accountDeps.env } : {}),
|
|
212
|
+
});
|
|
213
|
+
if (status.logged_in === true)
|
|
210
214
|
proven.push(id);
|
|
211
215
|
}
|
|
212
216
|
if (proven.length === 0) {
|
|
@@ -235,6 +239,9 @@ export function selectBalancedAccount(provider, config, ctx = {}) {
|
|
|
235
239
|
const cap = accountMaxRunning(providerCfg, id);
|
|
236
240
|
return runningLaneCount(lanes, provider, id) < cap;
|
|
237
241
|
});
|
|
242
|
+
const preferred = underCap.length > 0
|
|
243
|
+
? ctx.preferredAccounts?.find((id) => underCap.includes(id))
|
|
244
|
+
: undefined;
|
|
238
245
|
const pool = underCap.length > 0 ? underCap : runnable;
|
|
239
246
|
const ranked = pool
|
|
240
247
|
.map((id) => ({
|
|
@@ -243,7 +250,7 @@ export function selectBalancedAccount(provider, config, ctx = {}) {
|
|
|
243
250
|
lastStarted: lastStartedAt(lanes, provider, id),
|
|
244
251
|
}))
|
|
245
252
|
.sort(compareEligible);
|
|
246
|
-
const chosen = ranked[0].id;
|
|
253
|
+
const chosen = preferred ?? ranked[0].id;
|
|
247
254
|
return boundAccount(provider, chosen, providerCfg.accounts[chosen]);
|
|
248
255
|
}
|
|
249
256
|
export function resolveAccount(provider, requestedId, config, ctx = {}) {
|
|
@@ -293,14 +300,28 @@ export function accountStatus(provider, accountId, config, deps = defaultAccount
|
|
|
293
300
|
};
|
|
294
301
|
}
|
|
295
302
|
const present = hasCredentialFile(provider, account.home, deps.existsSync);
|
|
296
|
-
|
|
303
|
+
let auth_state = authStateFor(provider, present);
|
|
304
|
+
let logged_in = loggedInFromAuthState(auth_state);
|
|
305
|
+
if (!present && provider === "claude" && deps.probe !== undefined) {
|
|
306
|
+
try {
|
|
307
|
+
const result = deps.probe("claude", ["auth", "status"], { ...(deps.env ?? process.env), CLAUDE_CONFIG_DIR: account.home });
|
|
308
|
+
const providerStatus = claudeLoginStatus(result);
|
|
309
|
+
if (providerStatus !== null) {
|
|
310
|
+
auth_state = "status";
|
|
311
|
+
logged_in = providerStatus;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
process.stderr.write(`hq lanes: Claude auth status probe failed for account ${accountId}\n`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
297
318
|
const status = {
|
|
298
319
|
provider,
|
|
299
320
|
id: accountId,
|
|
300
321
|
home: account.home,
|
|
301
322
|
enabled: account.enabled,
|
|
302
323
|
configured: true,
|
|
303
|
-
logged_in
|
|
324
|
+
logged_in,
|
|
304
325
|
auth_state,
|
|
305
326
|
envVar,
|
|
306
327
|
};
|
|
@@ -312,6 +333,27 @@ export function accountStatus(provider, accountId, config, deps = defaultAccount
|
|
|
312
333
|
export function listAccountStatuses(provider, config, deps = defaultAccountDeps()) {
|
|
313
334
|
return Object.keys(config.providers[provider].accounts).map((id) => accountStatus(provider, id, config, deps));
|
|
314
335
|
}
|
|
336
|
+
function claudeLoginStatus(result) {
|
|
337
|
+
const text = result.stdout.trim();
|
|
338
|
+
if (text.startsWith("{")) {
|
|
339
|
+
try {
|
|
340
|
+
const parsed = JSON.parse(text);
|
|
341
|
+
if (parsed !== null &&
|
|
342
|
+
typeof parsed === "object" &&
|
|
343
|
+
!Array.isArray(parsed) &&
|
|
344
|
+
typeof parsed.loggedIn === "boolean") {
|
|
345
|
+
return parsed.loggedIn;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
process.stderr.write(`hq lanes: Claude auth status returned invalid JSON: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (/\bnot logged in\b/i.test(`${result.stdout}\n${result.stderr}`))
|
|
354
|
+
return false;
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
315
357
|
/** Environment assignment the worker process should receive. */
|
|
316
358
|
export function accountEnv(resolved) {
|
|
317
359
|
return { [resolved.envVar]: resolved.home };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AdmissionHost } from "./admission.js";
|
|
2
|
+
export declare const OUTPOST_ID_PATH = "/etc/outpost/outpost-id";
|
|
3
|
+
export declare const OUTPOST_DIR = "/etc/outpost";
|
|
4
|
+
export type OutpostIdentity = {
|
|
5
|
+
kind: "id";
|
|
6
|
+
id: string;
|
|
7
|
+
} | {
|
|
8
|
+
kind: "unknown";
|
|
9
|
+
} | {
|
|
10
|
+
kind: "not_outpost";
|
|
11
|
+
};
|
|
12
|
+
export declare function readOutpostIdentity(existsSync?: (p: string) => boolean, readFileSync?: (p: string) => string): OutpostIdentity;
|
|
13
|
+
export declare function resolveAdmissionHost(deps?: {
|
|
14
|
+
hostname?: () => string;
|
|
15
|
+
readOutpostIdentity?: () => OutpostIdentity;
|
|
16
|
+
existsSync?: (p: string) => boolean;
|
|
17
|
+
readFileSync?: (p: string) => string;
|
|
18
|
+
}): AdmissionHost;
|
|
19
|
+
//# sourceMappingURL=admission-host.d.ts.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
export const OUTPOST_ID_PATH = "/etc/outpost/outpost-id";
|
|
4
|
+
export const OUTPOST_DIR = "/etc/outpost";
|
|
5
|
+
export function readOutpostIdentity(existsSync = (p) => fs.existsSync(p), readFileSync = (p) => fs.readFileSync(p, "utf8")) {
|
|
6
|
+
const hasMarker = existsSync(OUTPOST_ID_PATH);
|
|
7
|
+
const hasDir = existsSync(OUTPOST_DIR);
|
|
8
|
+
if (hasMarker) {
|
|
9
|
+
let text;
|
|
10
|
+
try {
|
|
11
|
+
text = readFileSync(OUTPOST_ID_PATH);
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
process.stderr.write(`hq lanes: failed to read ${OUTPOST_ID_PATH}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
15
|
+
throw err;
|
|
16
|
+
}
|
|
17
|
+
const id = text.trim();
|
|
18
|
+
if (!id)
|
|
19
|
+
return hasDir ? { kind: "unknown" } : { kind: "not_outpost" };
|
|
20
|
+
return { kind: "id", id };
|
|
21
|
+
}
|
|
22
|
+
if (hasDir)
|
|
23
|
+
return { kind: "unknown" };
|
|
24
|
+
return { kind: "not_outpost" };
|
|
25
|
+
}
|
|
26
|
+
export function resolveAdmissionHost(deps = {}) {
|
|
27
|
+
const identity = deps.readOutpostIdentity?.() ?? readOutpostIdentity(deps.existsSync, deps.readFileSync);
|
|
28
|
+
return {
|
|
29
|
+
hostname: (deps.hostname ?? os.hostname)(),
|
|
30
|
+
outpostId: identity.kind === "id" ? identity.id : null,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=admission-host.js.map
|
package/dist/lib/lanes/config.js
CHANGED
|
@@ -173,6 +173,29 @@ function parseFastModeModels(value, field) {
|
|
|
173
173
|
}
|
|
174
174
|
return value.map((model) => model.trim());
|
|
175
175
|
}
|
|
176
|
+
function parsePreferredAccounts(provider, value, accounts) {
|
|
177
|
+
const field = `providers.${provider}.preferred_accounts`;
|
|
178
|
+
if (value === undefined)
|
|
179
|
+
return [];
|
|
180
|
+
if (!Array.isArray(value) ||
|
|
181
|
+
value.some((id) => typeof id !== "string" ||
|
|
182
|
+
id.trim().length === 0 ||
|
|
183
|
+
id.trim() !== id)) {
|
|
184
|
+
throw new LanesConfigError("invalid_preferred_accounts", `${field} must be an array of non-empty account ids without surrounding whitespace`, { provider, field });
|
|
185
|
+
}
|
|
186
|
+
const preferred = value;
|
|
187
|
+
const seen = new Set();
|
|
188
|
+
for (const id of preferred) {
|
|
189
|
+
if (seen.has(id)) {
|
|
190
|
+
throw new LanesConfigError("invalid_preferred_accounts", `${field} contains duplicate account ${JSON.stringify(id)}`, { provider, field, account: id });
|
|
191
|
+
}
|
|
192
|
+
if (accounts[id] === undefined) {
|
|
193
|
+
throw new LanesConfigError("preferred_account_unconfigured", `${field} names unconfigured account ${JSON.stringify(id)}`, { provider, field, account: id, accounts: Object.keys(accounts) });
|
|
194
|
+
}
|
|
195
|
+
seen.add(id);
|
|
196
|
+
}
|
|
197
|
+
return [...preferred];
|
|
198
|
+
}
|
|
176
199
|
function rejectDuplicateHomes(provider, accounts) {
|
|
177
200
|
const seen = new Map();
|
|
178
201
|
for (const [id, account] of Object.entries(accounts)) {
|
|
@@ -232,6 +255,7 @@ function parseProvider(provider, raw, deps) {
|
|
|
232
255
|
max_running: positiveInt(raw.max_running, `providers.${provider}.max_running`, DEFAULT_PROVIDER_MAX_RUNNING[provider]),
|
|
233
256
|
max_queued: positiveInt(raw.max_queued, `providers.${provider}.max_queued`, DEFAULT_MAX_QUEUED),
|
|
234
257
|
accounts,
|
|
258
|
+
preferred_accounts: parsePreferredAccounts(provider, raw.preferred_accounts, accounts),
|
|
235
259
|
};
|
|
236
260
|
if (provider === "codex") {
|
|
237
261
|
parsed.fast_mode_models = parseFastModeModels(raw.fast_mode_models, `providers.${provider}.fast_mode_models`);
|
|
@@ -301,6 +325,7 @@ export function documentedDefaultConfig(deps = defaultDeps()) {
|
|
|
301
325
|
max_running: DEFAULT_PROVIDER_MAX_RUNNING[provider],
|
|
302
326
|
max_queued: DEFAULT_MAX_QUEUED,
|
|
303
327
|
accounts: defaultAccountMap(provider, deps),
|
|
328
|
+
preferred_accounts: [],
|
|
304
329
|
};
|
|
305
330
|
if (provider === "codex")
|
|
306
331
|
providerConfig.fast_mode_models = [];
|
|
@@ -42,11 +42,11 @@
|
|
|
42
42
|
* of opening a browser nobody is watching, and the watcher records
|
|
43
43
|
* `mesh_event_failed` and moves on. No credential file is read here.
|
|
44
44
|
*/
|
|
45
|
-
import type { LaneCompanyRef, LaneEnvelope, LaneRecord } from "./types.js";
|
|
46
|
-
export declare const LANE_MESH_EVENTS: readonly ["lane_started", "lane_progress", "lane_question", "lane_question_at_user", "lane_ask", "lane_blocked", "lane_done"];
|
|
45
|
+
import type { LaneCompanyRef, LaneEnvelope, LaneRecord, TerminalLaneState } from "./types.js";
|
|
46
|
+
export declare const LANE_MESH_EVENTS: readonly ["lane_started", "lane_progress", "lane_question", "lane_question_at_user", "lane_ask", "lane_blocked", "lane_done", "lane_failed", "lane_interrupted", "lane_cancelled", "lane_killed"];
|
|
47
47
|
export type LaneMeshEvent = (typeof LANE_MESH_EVENTS)[number];
|
|
48
48
|
/** Roster kinds this module emits. Subset of the server's `validKinds`. */
|
|
49
|
-
export declare const WIRE_EVENT_KINDS: readonly ["run_started", "run_progress", "run_complete", "question", "note"];
|
|
49
|
+
export declare const WIRE_EVENT_KINDS: readonly ["run_started", "run_progress", "run_complete", "blocked", "question", "note"];
|
|
50
50
|
export type WireEventKind = (typeof WIRE_EVENT_KINDS)[number];
|
|
51
51
|
/** Server caps: activity title/summary 2000, question text 500, note text 500. */
|
|
52
52
|
export declare const WIRE_ACTIVITY_TEXT_MAX = 2000;
|
|
@@ -70,6 +70,7 @@ export declare function toWireThreadEvent(event: LaneMeshEvent, payload: Record<
|
|
|
70
70
|
*/
|
|
71
71
|
export interface LaneMesh {
|
|
72
72
|
ensureProjectThread: (company: LaneCompanyRef, projectId: string) => Promise<string>;
|
|
73
|
+
ensureLaneThread?: (company: LaneCompanyRef, lane: LaneRecord, laneStatus?: string, signal?: AbortSignal) => Promise<string>;
|
|
73
74
|
appendThreadEvent: (company: LaneCompanyRef, threadId: string, event: LaneMeshEvent, payload: Record<string, unknown>, signal?: AbortSignal) => Promise<void>;
|
|
74
75
|
patchStoryStatus: (company: LaneCompanyRef, projectId: string, storyId: string, status: string) => Promise<void>;
|
|
75
76
|
}
|
|
@@ -92,6 +93,7 @@ export declare function createLaneMesh(opts?: CreateLaneMeshOptions): LaneMesh;
|
|
|
92
93
|
export declare function laneMeshCompany(lane: Pick<LaneRecord, "company" | "mesh_company">): LaneCompanyRef | undefined;
|
|
93
94
|
/** Company attribution + thread present. Unscoped personal lanes emit nothing. */
|
|
94
95
|
export declare function laneCanEmit(lane: Pick<LaneRecord, "company" | "mesh_company" | "thread_id">): boolean;
|
|
96
|
+
export declare function laneMeshEventIdentity(lane: LaneRecord): Record<string, unknown>;
|
|
95
97
|
/**
|
|
96
98
|
* Spec §8 terminal-envelope events. `done` → lane_done and story → review;
|
|
97
99
|
* `blocked` → lane_blocked; `ask` → lane_ask with the queue ids. The story is
|
|
@@ -101,6 +103,8 @@ export declare function emitLaneDecisionEvents(mesh: LaneMesh, lane: LaneRecord,
|
|
|
101
103
|
questionIds?: string[];
|
|
102
104
|
signal?: AbortSignal;
|
|
103
105
|
}): Promise<void>;
|
|
106
|
+
/** Emit an abnormal terminal state even when the worker produced no envelope. */
|
|
107
|
+
export declare function emitLaneTerminalStateEvent(mesh: LaneMesh, lane: LaneRecord, state: Exclude<TerminalLaneState, "done">, summary?: string, signal?: AbortSignal): Promise<void>;
|
|
104
108
|
export declare function emitLaneProgressEvent(mesh: LaneMesh, lane: LaneRecord, detail: string, signal?: AbortSignal): Promise<void>;
|
|
105
109
|
/**
|
|
106
110
|
* Throttle for lane_progress (spec §8 progress row). True when no progress
|