@indigoai-us/hq-cli 5.345.123 → 5.345.125
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 +39 -0
- package/assets/scaffold/core/scripts/work-mesh-project-registration-offer.sh +77 -0
- package/assets/scaffold/core/scripts/workflow-gate.sh +247 -0
- package/dist/command-catalog.generated.d.ts +239 -0
- package/dist/command-catalog.generated.js +291 -0
- package/dist/commands/core-bucket-c-work-mesh-offer-native.d.ts +7 -0
- package/dist/commands/core-bucket-c-work-mesh-offer-native.js +147 -0
- package/dist/commands/core-bucket-c-workflow-gate-fast.d.ts +3 -0
- package/dist/commands/core-bucket-c-workflow-gate-fast.js +17 -0
- package/dist/commands/core-bucket-c-workflow-gate-native.d.ts +7 -0
- package/dist/commands/core-bucket-c-workflow-gate-native.js +250 -0
- package/dist/commands/core.js +12 -0
- package/dist/commands/lanes-accounts.js +22 -3
- package/dist/commands/lanes.js +25 -32
- package/dist/commands/mesh.js +457 -16
- package/dist/fast-scaffold.js +26 -0
- package/dist/lib/lanes/accounts.d.ts +48 -2
- package/dist/lib/lanes/accounts.js +160 -9
- package/dist/lib/lanes/index.d.ts +2 -2
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/message-replay.js +6 -1
- package/dist/lib/lanes/provider-rejection.d.ts +2 -6
- package/dist/lib/lanes/provider-rejection.js +19 -0
- package/dist/lib/lanes/quota-notice.d.ts +24 -0
- package/dist/lib/lanes/quota-notice.js +61 -0
- package/dist/lib/lanes/quota.d.ts +55 -15
- package/dist/lib/lanes/quota.js +231 -49
- package/dist/lib/lanes/reconcile.js +17 -1
- package/dist/lib/lanes/session-relink.d.ts +2 -0
- package/dist/lib/lanes/session-relink.js +98 -4
- package/dist/lib/lanes/spawn.d.ts +1 -1
- package/dist/lib/lanes/spawn.js +58 -12
- package/dist/lib/lanes/types.d.ts +5 -0
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/lanes/watcher.js +13 -1
- package/dist/lib/mesh/api.d.ts +2 -0
- package/dist/lib/mesh/api.js +3 -0
- package/dist/lib/mesh/client.d.ts +6 -1
- package/dist/lib/mesh/client.js +18 -5
- package/dist/lib/mesh/team-coordination.d.ts +106 -0
- package/dist/lib/mesh/team-coordination.js +297 -0
- package/dist/lib/work-context/auto-bind.d.ts +75 -0
- package/dist/lib/work-context/auto-bind.js +245 -0
- package/package.json +1 -1
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
const ok = (stdout = "") => ({ code: 0, stdout, stderr: "" });
|
|
4
|
+
const fail = (message, code = 1) => ({ code, stdout: "", stderr: `${message}\n` });
|
|
5
|
+
const validId = /^[a-z0-9._-]+$/;
|
|
6
|
+
const USAGE = `workflow-gate.sh — inspect and answer human gates opened by codex-workflow.mjs gate().
|
|
7
|
+
|
|
8
|
+
A gate is a self-contained pending question file written by a paused workflow
|
|
9
|
+
run; writing a matching answered/<id>.json resumes that run in place. Answers
|
|
10
|
+
are durable: a re-launched run returns them instantly instead of re-asking
|
|
11
|
+
the human, so never delete answered/ files while their project is live.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
workflow-gate.sh list
|
|
15
|
+
workflow-gate.sh show <id>
|
|
16
|
+
workflow-gate.sh answer <id> <choice|N> [--notes "..."] [--by name] [--force] [--freeform]
|
|
17
|
+
<choice|N> an option label, or a 1-based option index
|
|
18
|
+
--notes free-text note stored alongside the choice
|
|
19
|
+
--by who answered (recorded as answered_by)
|
|
20
|
+
--force pre-answer a gate that has not opened yet, or overwrite an
|
|
21
|
+
existing answer
|
|
22
|
+
--freeform accept a choice that is not one of the gate's options
|
|
23
|
+
workflow-gate.sh wait-pending [--timeout secs]
|
|
24
|
+
Blocks until any pending gate exists (exit 0) or the timeout elapses
|
|
25
|
+
(exit 1). Monitor-friendly wake condition for watching sessions.
|
|
26
|
+
workflow-gate.sh watch-run <run-dir> [--timeout secs]
|
|
27
|
+
Run-scoped wake condition for the session that launched a workflow.
|
|
28
|
+
Blocks until a gate belonging to THAT run opens (prints its id, exit 0),
|
|
29
|
+
the run finishes (exit 3), or --timeout elapses (exit 1). Match is on
|
|
30
|
+
the run_dir every gate carries, which is the --run-dir the launching
|
|
31
|
+
session chose — no id parsing, no env coupling, no startup race.
|
|
32
|
+
workflow-gate.sh clear <id>
|
|
33
|
+
Remove both the pending and answered files for a gate id.
|
|
34
|
+
|
|
35
|
+
Env: HQ_WORKFLOW_GATES_DIR overrides the gates root (default
|
|
36
|
+
<hq-root>/workspace/gates); legacy CODEX_WORKFLOW_GATES_DIR is honored.
|
|
37
|
+
`;
|
|
38
|
+
function gateId(id) {
|
|
39
|
+
return !validId.test(id) ? fail(`workflow-gate: invalid gate id "${id}" — ids are lowercase [a-z0-9._-]`) : undefined;
|
|
40
|
+
}
|
|
41
|
+
function readJson(file) {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function listFiles(dir) {
|
|
50
|
+
try {
|
|
51
|
+
return fs.readdirSync(dir).filter((entry) => entry.endsWith(".json")).sort().map((entry) => path.join(dir, entry));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function parseTimeout(args) {
|
|
58
|
+
let timeout;
|
|
59
|
+
for (let index = 0; index < args.length;) {
|
|
60
|
+
if (args[index] !== "--timeout")
|
|
61
|
+
return { error: `workflow-gate: unknown flag: ${args[index]}` };
|
|
62
|
+
if (index + 1 >= args.length)
|
|
63
|
+
return { error: "workflow-gate: --timeout needs a value" };
|
|
64
|
+
const raw = args[index + 1];
|
|
65
|
+
if (!/^\d+$/.test(raw))
|
|
66
|
+
return { error: "workflow-gate: --timeout must be a whole number of seconds" };
|
|
67
|
+
timeout = Number(raw);
|
|
68
|
+
index += 2;
|
|
69
|
+
}
|
|
70
|
+
return { ...(timeout === undefined ? {} : { timeout }) };
|
|
71
|
+
}
|
|
72
|
+
const waitSecond = () => new Promise((resolve) => setTimeout(resolve, 1000));
|
|
73
|
+
export async function runWorkflowGate(args, root, env = process.env, now = Date.now, sleep = waitSecond) {
|
|
74
|
+
const gatesDir = env.HQ_WORKFLOW_GATES_DIR || env.CODEX_WORKFLOW_GATES_DIR || path.join(root, "workspace/gates");
|
|
75
|
+
const pending = path.join(gatesDir, "pending");
|
|
76
|
+
const answered = path.join(gatesDir, "answered");
|
|
77
|
+
const [command, ...rest] = args;
|
|
78
|
+
if (!command)
|
|
79
|
+
return { code: 1, stdout: `${USAGE}\n`, stderr: "" };
|
|
80
|
+
if (command === "-h" || command === "--help" || command === "help")
|
|
81
|
+
return ok(`${USAGE}\n`);
|
|
82
|
+
if (command === "list") {
|
|
83
|
+
const files = listFiles(pending);
|
|
84
|
+
if (!files.length)
|
|
85
|
+
return ok("no pending gates\n");
|
|
86
|
+
return ok(files.map((file) => {
|
|
87
|
+
const gate = readJson(file);
|
|
88
|
+
if (!gate || typeof gate !== "object")
|
|
89
|
+
return `<unreadable> ${file}`;
|
|
90
|
+
const created = Date.parse(String(gate.created_at || 0));
|
|
91
|
+
const ageMin = Math.max(0, Math.round((now() - created) / 60000));
|
|
92
|
+
const options = Array.isArray(gate.options) ? gate.options.map((option, index) => `${index + 1}=${String(option?.label)}`).join(" ") : "";
|
|
93
|
+
return `${String(gate.id)} (${ageMin}m) ${String(gate.question)}${options ? ` [${options}]` : ""}`;
|
|
94
|
+
}).join("\n") + "\n");
|
|
95
|
+
}
|
|
96
|
+
if (command === "show") {
|
|
97
|
+
const id = rest[0] ?? "";
|
|
98
|
+
if (!id)
|
|
99
|
+
return fail("workflow-gate: usage: show <id>");
|
|
100
|
+
const invalid = gateId(id);
|
|
101
|
+
if (invalid)
|
|
102
|
+
return invalid;
|
|
103
|
+
const file = [path.join(pending, `${id}.json`), path.join(answered, `${id}.json`)].find((candidate) => {
|
|
104
|
+
try {
|
|
105
|
+
return fs.statSync(candidate).isFile();
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
if (!file)
|
|
112
|
+
return fail(`workflow-gate: no gate "${id}" in pending or answered`);
|
|
113
|
+
try {
|
|
114
|
+
return ok(fs.readFileSync(file, "utf8"));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return fail(`workflow-gate: no gate "${id}" in pending or answered`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (command === "answer") {
|
|
121
|
+
const [id = "", rawChoice = "", ...flags] = rest;
|
|
122
|
+
if (!id || !rawChoice)
|
|
123
|
+
return fail("workflow-gate: usage: answer <id> <choice|N> [--notes ...] [--by name] [--force] [--freeform]");
|
|
124
|
+
const invalid = gateId(id);
|
|
125
|
+
if (invalid)
|
|
126
|
+
return invalid;
|
|
127
|
+
let notes = "";
|
|
128
|
+
let by = "";
|
|
129
|
+
let force = false;
|
|
130
|
+
let freeform = false;
|
|
131
|
+
for (let index = 0; index < flags.length;) {
|
|
132
|
+
const flag = flags[index];
|
|
133
|
+
if (flag === "--force") {
|
|
134
|
+
force = true;
|
|
135
|
+
index += 1;
|
|
136
|
+
}
|
|
137
|
+
else if (flag === "--freeform") {
|
|
138
|
+
freeform = true;
|
|
139
|
+
index += 1;
|
|
140
|
+
}
|
|
141
|
+
else if (flag === "--notes" || flag === "--by") {
|
|
142
|
+
if (index + 1 >= flags.length)
|
|
143
|
+
return fail(`workflow-gate: ${flag} needs a value`);
|
|
144
|
+
if (flag === "--notes")
|
|
145
|
+
notes = flags[index + 1];
|
|
146
|
+
else
|
|
147
|
+
by = flags[index + 1];
|
|
148
|
+
index += 2;
|
|
149
|
+
}
|
|
150
|
+
else
|
|
151
|
+
return fail(`workflow-gate: unknown flag: ${flag}`);
|
|
152
|
+
}
|
|
153
|
+
const pendingFile = path.join(pending, `${id}.json`);
|
|
154
|
+
const answerFile = path.join(answered, `${id}.json`);
|
|
155
|
+
const gate = readJson(pendingFile);
|
|
156
|
+
if (!gate && !force) {
|
|
157
|
+
const ids = listFiles(pending).map((file) => path.basename(file, ".json"));
|
|
158
|
+
return fail(`workflow-gate: no pending gate "${id}"${ids.length ? ` — pending: ${ids.join(", ")}` : " — nothing is pending"} (use --force to pre-answer)`);
|
|
159
|
+
}
|
|
160
|
+
let choice = rawChoice;
|
|
161
|
+
const options = gate && Array.isArray(gate.options) ? gate.options : [];
|
|
162
|
+
if (options.length) {
|
|
163
|
+
if (/^\d+$/.test(rawChoice)) {
|
|
164
|
+
const number = Number(rawChoice);
|
|
165
|
+
if (number >= 1 && number <= options.length)
|
|
166
|
+
choice = String(options[number - 1].label);
|
|
167
|
+
else
|
|
168
|
+
return fail(`workflow-gate: index ${number} is out of range 1..${options.length} for "${id}"`);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
const hit = options.find((option) => String(option.label).toLowerCase() === rawChoice.toLowerCase());
|
|
172
|
+
if (hit)
|
|
173
|
+
choice = String(hit.label);
|
|
174
|
+
else if (!freeform)
|
|
175
|
+
return fail(`workflow-gate: "${rawChoice}" is not an option for "${id}" — options: ${options.map((option) => option.label).join(", ")} (use --freeform to record it anyway)`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (fs.existsSync(answerFile) && !force)
|
|
179
|
+
return fail(`workflow-gate: gate "${id}" is already answered (${answerFile}) — use --force to overwrite`);
|
|
180
|
+
try {
|
|
181
|
+
fs.mkdirSync(answered, { recursive: true });
|
|
182
|
+
const temporary = `${answerFile}.tmp-${process.pid}`;
|
|
183
|
+
fs.writeFileSync(temporary, `${JSON.stringify({ id, choice, ...(notes ? { notes } : {}), ...(by ? { answered_by: by } : {}), answered_at: new Date(now()).toISOString() }, null, 2)}\n`);
|
|
184
|
+
fs.renameSync(temporary, answerFile);
|
|
185
|
+
try {
|
|
186
|
+
fs.rmSync(pendingFile, { force: true });
|
|
187
|
+
}
|
|
188
|
+
catch { /* shell is best-effort here */ }
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
return fail(`workflow-gate: ${String(error.message)}`);
|
|
192
|
+
}
|
|
193
|
+
return ok(`answered ${id} -> ${choice}\n`);
|
|
194
|
+
}
|
|
195
|
+
if (command === "wait-pending" || command === "watch-run") {
|
|
196
|
+
let runDir = "";
|
|
197
|
+
let optionArgs = rest;
|
|
198
|
+
if (command === "watch-run") {
|
|
199
|
+
runDir = rest[0] ?? "";
|
|
200
|
+
if (!runDir || runDir.startsWith("-"))
|
|
201
|
+
return fail("workflow-gate: usage: watch-run <run-dir> [--timeout secs]");
|
|
202
|
+
optionArgs = rest.slice(1);
|
|
203
|
+
}
|
|
204
|
+
const parsed = parseTimeout(optionArgs);
|
|
205
|
+
if (parsed.error)
|
|
206
|
+
return fail(parsed.error);
|
|
207
|
+
let elapsed = 0;
|
|
208
|
+
for (;;) {
|
|
209
|
+
const files = listFiles(pending);
|
|
210
|
+
if (command === "wait-pending" && files.length)
|
|
211
|
+
return ok();
|
|
212
|
+
if (command === "watch-run") {
|
|
213
|
+
const wanted = path.resolve(runDir);
|
|
214
|
+
for (const file of files) {
|
|
215
|
+
const gate = readJson(file);
|
|
216
|
+
if (gate?.run_dir && path.resolve(String(gate.run_dir)) === wanted)
|
|
217
|
+
return ok(`${String(gate.id || path.basename(file, ".json"))}\n`);
|
|
218
|
+
}
|
|
219
|
+
const journal = path.join(runDir, "journal.jsonl");
|
|
220
|
+
try {
|
|
221
|
+
if (fs.readFileSync(journal, "utf8").includes('"event":"run-done"'))
|
|
222
|
+
return { code: 3, stdout: "", stderr: "" };
|
|
223
|
+
}
|
|
224
|
+
catch { /* not finished */ }
|
|
225
|
+
}
|
|
226
|
+
if (parsed.timeout !== undefined && elapsed >= parsed.timeout)
|
|
227
|
+
return { code: 1, stdout: "", stderr: "" };
|
|
228
|
+
await sleep();
|
|
229
|
+
elapsed += 1;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (command === "clear") {
|
|
233
|
+
const id = rest[0] ?? "";
|
|
234
|
+
if (!id)
|
|
235
|
+
return fail("workflow-gate: usage: clear <id>");
|
|
236
|
+
const invalid = gateId(id);
|
|
237
|
+
if (invalid)
|
|
238
|
+
return invalid;
|
|
239
|
+
try {
|
|
240
|
+
fs.rmSync(path.join(pending, `${id}.json`), { force: true });
|
|
241
|
+
fs.rmSync(path.join(answered, `${id}.json`), { force: true });
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
return fail(`workflow-gate: ${String(error.message)}`);
|
|
245
|
+
}
|
|
246
|
+
return ok(`cleared ${id}\n`);
|
|
247
|
+
}
|
|
248
|
+
return fail(`workflow-gate: unknown command: ${command} (list | show | answer | wait-pending | watch-run | clear)`);
|
|
249
|
+
}
|
|
250
|
+
//# sourceMappingURL=core-bucket-c-workflow-gate-native.js.map
|
package/dist/commands/core.js
CHANGED
|
@@ -248,6 +248,18 @@ export const SCAFFOLD_COMMANDS = [
|
|
|
248
248
|
root: "live",
|
|
249
249
|
summary: "Check whether a company-scoped skill is installed",
|
|
250
250
|
},
|
|
251
|
+
{
|
|
252
|
+
name: "work-mesh-project-registration-offer",
|
|
253
|
+
asset: "core/scripts/work-mesh-project-registration-offer.sh",
|
|
254
|
+
root: "live",
|
|
255
|
+
summary: "Check and record the work-mesh project registration offer",
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: "workflow-gate",
|
|
259
|
+
asset: "core/scripts/workflow-gate.sh",
|
|
260
|
+
root: "live",
|
|
261
|
+
summary: "Inspect, answer, and wait for workflow gates",
|
|
262
|
+
},
|
|
251
263
|
// Session/harness plumbing relocated out of the HQ tree so a single published
|
|
252
264
|
// CLI owns it. Both are `root: "live"`: they read and write the live HQ tree
|
|
253
265
|
// (workspace/sessions, companies/, workspace/orchestrator/hook-state), so the
|
|
@@ -4,7 +4,7 @@ import { buildOutpostBalanceMoveCommand, dispatchErrorMessage } from "../lib/lan
|
|
|
4
4
|
import { BALANCE_MOVABLE_STATES, accountLoadsForReport, planClaudeBalanceMoves, selectClaudeAccount } from "../lib/lanes/claude-balance.js";
|
|
5
5
|
import { CompanySelectionError } from "../utils/company-selection-error.js";
|
|
6
6
|
import { effectiveAccountHome } from "../lib/lanes/account-home.js";
|
|
7
|
-
import { AccountResolutionError, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS,
|
|
7
|
+
import { AccountResolutionError, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, accountStatus, canonicalizeHome, claudeIdentityDiffers, installDrainHook, isLanesError, listAccountStatuses, loadLanesConfig, parseLaneProvider, probeInteractiveClaudeIdentity, readLane, withProviderHomeEnv, writeLanesConfig } from "../lib/lanes/index.js";
|
|
8
8
|
import { assertUniqueAccountHomes, providerCompactAfterMs } from "../lib/lanes/config.js";
|
|
9
9
|
import { resolveLaneEffort } from "../lib/lanes/effort.js";
|
|
10
10
|
import { LanesError } from "../lib/lanes/errors.js";
|
|
@@ -113,9 +113,16 @@ export function createLaneAccountsHandlers(deps) {
|
|
|
113
113
|
// model and effort are what a spawn on each account would start from
|
|
114
114
|
// (spec: --model/--effort, then the account, then the provider).
|
|
115
115
|
const quota = loadQuotaState(hqRoot);
|
|
116
|
+
// The login the interactive `claude` uses (no CLAUDE_CONFIG_DIR), so a
|
|
117
|
+
// lane account that resolves to a different keychain credential is
|
|
118
|
+
// visible here instead of only as a 429 on the wrong subscription.
|
|
119
|
+
const interactiveIdentity = providers.includes("claude") && ctx.probeProvider
|
|
120
|
+
? probeInteractiveClaudeIdentity(ctx.probeProvider, ctx.env)
|
|
121
|
+
: null;
|
|
116
122
|
const accounts = providers.flatMap((provider) => listAccountStatuses(provider, config, {
|
|
117
123
|
existsSync: ctx.existsSync,
|
|
118
124
|
hostname: ctx.hostname(),
|
|
125
|
+
homedir: ctx.homedir,
|
|
119
126
|
probe: ctx.probeProvider,
|
|
120
127
|
env: ctx.env,
|
|
121
128
|
}).map((status) => {
|
|
@@ -129,6 +136,9 @@ export function createLaneAccountsHandlers(deps) {
|
|
|
129
136
|
providerEffort: config.providers[provider].effort,
|
|
130
137
|
model,
|
|
131
138
|
});
|
|
139
|
+
const differs = provider === "claude"
|
|
140
|
+
? claudeIdentityDiffers(status.identity, interactiveIdentity)
|
|
141
|
+
: null;
|
|
132
142
|
return {
|
|
133
143
|
...status,
|
|
134
144
|
effective_home: status.home,
|
|
@@ -136,6 +146,13 @@ export function createLaneAccountsHandlers(deps) {
|
|
|
136
146
|
error: "home_missing",
|
|
137
147
|
fix_command: `hq lanes accounts set-home ${provider} ${status.id} <host-home>`,
|
|
138
148
|
} : {}),
|
|
149
|
+
...(provider === "claude" ? {
|
|
150
|
+
identity: status.identity ?? null,
|
|
151
|
+
credential_matches_interactive: differs === null ? null : !differs,
|
|
152
|
+
...(differs === true ? {
|
|
153
|
+
warning: `Lane credential ${status.identity?.email ?? status.identity?.org_id ?? "(unknown)"} differs from the interactive claude login ${interactiveIdentity?.email ?? interactiveIdentity?.org_id ?? "(unknown)"}; lanes on this account bill that other subscription`,
|
|
154
|
+
} : {}),
|
|
155
|
+
} : {}),
|
|
139
156
|
model: accountCfg?.model ?? null,
|
|
140
157
|
effort: accountCfg?.effort ?? null,
|
|
141
158
|
effective_model: model ?? null,
|
|
@@ -154,6 +171,7 @@ export function createLaneAccountsHandlers(deps) {
|
|
|
154
171
|
ok: true,
|
|
155
172
|
action: "accounts.list",
|
|
156
173
|
providers: providerDefaults,
|
|
174
|
+
...(providers.includes("claude") ? { interactive_identity: interactiveIdentity } : {}),
|
|
157
175
|
accounts,
|
|
158
176
|
});
|
|
159
177
|
}
|
|
@@ -387,15 +405,16 @@ export function createLaneAccountsHandlers(deps) {
|
|
|
387
405
|
});
|
|
388
406
|
const copied = defaultHome !== home ? copyHookRails(defaultHome, home) : railsPresent(home);
|
|
389
407
|
installDrainHook(provider, home);
|
|
390
|
-
const envVar = PROVIDER_HOME_ENV[provider];
|
|
391
408
|
const login = VENDOR_LOGIN[provider];
|
|
392
409
|
const loginArgs = provider === "codex" && opts.deviceAuth
|
|
393
410
|
? [...login.args, "--device-auth"]
|
|
394
411
|
: login.args;
|
|
412
|
+
// Same rule as the worker spawn: the default Claude home logs in with
|
|
413
|
+
// CLAUDE_CONFIG_DIR unset so the credential matches the interactive one.
|
|
395
414
|
const result = await ctx.spawnLogin({
|
|
396
415
|
command: login.command,
|
|
397
416
|
args: loginArgs,
|
|
398
|
-
env:
|
|
417
|
+
env: withProviderHomeEnv(ctx.env, provider, home, ctx.homedir),
|
|
399
418
|
cwd: home,
|
|
400
419
|
});
|
|
401
420
|
if (result.status !== 0) {
|
package/dist/commands/lanes.js
CHANGED
|
@@ -2331,6 +2331,27 @@ function orchestratorEngine(hqRoot, senior, ctx) {
|
|
|
2331
2331
|
return "claude";
|
|
2332
2332
|
return undefined;
|
|
2333
2333
|
}
|
|
2334
|
+
/**
|
|
2335
|
+
* Starts the worker lane a lead autopilot fill-grant wake asks for. Provider,
|
|
2336
|
+
* model and effort are left to `runCreate`, so the worker follows the
|
|
2337
|
+
* orchestrator engine (the lead lane provider, then the harness markers) and
|
|
2338
|
+
* that provider default model, and fails with `provider_required` when no
|
|
2339
|
+
* engine is known. There is no hard-coded engine.
|
|
2340
|
+
*/
|
|
2341
|
+
function autopilotWorkerStarter(ctx) {
|
|
2342
|
+
return async ({ lead, story }) => {
|
|
2343
|
+
const workerCtx = { ...ctx, env: { ...ctx.env, HQ_LANE_ID: lead.lane_id } };
|
|
2344
|
+
const started = await runCreate(workerCtx, {
|
|
2345
|
+
company: lead.company?.slug,
|
|
2346
|
+
project: lead.project_id,
|
|
2347
|
+
story: story.id,
|
|
2348
|
+
briefFile: story.briefPath,
|
|
2349
|
+
senior: "lane:" + lead.lane_id,
|
|
2350
|
+
quiet: true,
|
|
2351
|
+
});
|
|
2352
|
+
return { lane_id: started.lane_id };
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2334
2355
|
function defaultSenior(ctx) {
|
|
2335
2356
|
const parent = ctx.env.HQ_PARENT_SESSION_ID?.trim();
|
|
2336
2357
|
if (parent)
|
|
@@ -7389,7 +7410,7 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
7389
7410
|
});
|
|
7390
7411
|
const block = quotaBlockFor(loadQuotaState(hqRoot), lane.provider, target, model, now);
|
|
7391
7412
|
if (block.blocked) {
|
|
7392
|
-
throw new LanesError("account_quota_held", `Account ${target} for ${lane.provider} is held${block.quota_reset_at ? ` until ${block.quota_reset_at}` : ""}${block.marker ? ` (${block.marker})` : ""}`, { provider: lane.provider, account: target, quota_reset_at: block.quota_reset_at ?? null, marker: block.marker ?? null, model: model ?? null });
|
|
7413
|
+
throw new LanesError("account_quota_held", `Account ${target} for ${lane.provider} is held${block.quota_reset_at ? ` until ${block.quota_reset_at}` : ""}${block.marker ? ` (${block.marker})` : ""}. Clear the hold with: hq lanes accounts clear-quota ${lane.provider} ${target}`, { provider: lane.provider, account: target, quota_reset_at: block.quota_reset_at ?? null, marker: block.marker ?? null, model: model ?? null, clear_command: `hq lanes accounts clear-quota ${lane.provider} ${target}` });
|
|
7393
7414
|
}
|
|
7394
7415
|
if (model !== undefined) {
|
|
7395
7416
|
validateLaneModel(lane.provider, model, targetHome, {
|
|
@@ -7407,7 +7428,7 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
7407
7428
|
const copy = ensureSessionHome({
|
|
7408
7429
|
provider: lane.provider,
|
|
7409
7430
|
sessionId,
|
|
7410
|
-
cwd: hqRoot,
|
|
7431
|
+
cwd: lane.working_directory_at_spawn ?? hqRoot,
|
|
7411
7432
|
targetHome,
|
|
7412
7433
|
registeredHomes: Object.values(providerCfg.accounts).map((account) => effectiveAccountHome(account, ctx.hostname()).home),
|
|
7413
7434
|
providerHomeAtSpawn: lane.provider_home_at_spawn ?? sourceHome,
|
|
@@ -9399,21 +9420,7 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
|
|
|
9399
9420
|
dryRun: Boolean(opts.dryRun),
|
|
9400
9421
|
readFlag: readLanesRegistryFlag,
|
|
9401
9422
|
keepAliveResumedLeads: new Set(summary.resumed.filter((entry) => entry.reason === "keep_alive").map((entry) => entry.id)),
|
|
9402
|
-
startWorker:
|
|
9403
|
-
const workerCtx = { ...ctx, env: { ...ctx.env, HQ_LANE_ID: lead.lane_id } };
|
|
9404
|
-
const started = await runCreate(workerCtx, {
|
|
9405
|
-
company: lead.company?.slug,
|
|
9406
|
-
project: lead.project_id,
|
|
9407
|
-
story: story.id,
|
|
9408
|
-
briefFile: story.briefPath,
|
|
9409
|
-
provider: "codex",
|
|
9410
|
-
model: "gpt-6-luna",
|
|
9411
|
-
effort: "high",
|
|
9412
|
-
senior: `lane:${lead.lane_id}`,
|
|
9413
|
-
quiet: true,
|
|
9414
|
-
});
|
|
9415
|
-
return { lane_id: started.lane_id };
|
|
9416
|
-
},
|
|
9423
|
+
startWorker: autopilotWorkerStarter(ctx),
|
|
9417
9424
|
});
|
|
9418
9425
|
}
|
|
9419
9426
|
catch (error) {
|
|
@@ -10446,21 +10453,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
10446
10453
|
now: ctx.now(),
|
|
10447
10454
|
dryRun: Boolean(opts.dryRun),
|
|
10448
10455
|
readFlag: readLanesRegistryFlag,
|
|
10449
|
-
startWorker:
|
|
10450
|
-
const workerCtx = { ...ctx, env: { ...ctx.env, HQ_LANE_ID: lead.lane_id } };
|
|
10451
|
-
const started = await runCreate(workerCtx, {
|
|
10452
|
-
company: lead.company?.slug,
|
|
10453
|
-
project: lead.project_id,
|
|
10454
|
-
story: story.id,
|
|
10455
|
-
briefFile: story.briefPath,
|
|
10456
|
-
provider: "codex",
|
|
10457
|
-
model: "gpt-6-luna",
|
|
10458
|
-
effort: "high",
|
|
10459
|
-
senior: `lane:${lead.lane_id}`,
|
|
10460
|
-
quiet: true,
|
|
10461
|
-
});
|
|
10462
|
-
return { lane_id: started.lane_id };
|
|
10463
|
-
},
|
|
10456
|
+
startWorker: autopilotWorkerStarter(ctx),
|
|
10464
10457
|
});
|
|
10465
10458
|
printJson(result);
|
|
10466
10459
|
}));
|