@indigoai-us/hq-cli 5.345.54 → 5.345.56
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 +31 -0
- package/assets/scaffold/core/scripts/derive-trigger-facts.sh +10 -1
- package/assets/scaffold/core/scripts/jobs-validate.sh +2 -1
- package/assets/scaffold/core/scripts/session-title.sh +18 -15
- package/dist/command-catalog.generated.d.ts +143 -2
- package/dist/command-catalog.generated.js +175 -2
- package/dist/command-registration-plan.d.ts +6 -1
- package/dist/command-registration-plan.js +2 -1
- package/dist/commands/cloud.d.ts +7 -0
- package/dist/commands/cloud.js +92 -16
- package/dist/commands/core-delegate-w1b.js +18 -5
- package/dist/commands/core-derive-trigger-facts-command.js +14 -0
- package/dist/commands/core-derive-trigger-facts-native.d.ts +1 -0
- package/dist/commands/core-derive-trigger-facts-native.js +20 -1
- package/dist/commands/daemon.js +16 -0
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +15 -2
- package/dist/commands/import-scan.d.ts +24 -0
- package/dist/commands/import-scan.js +75 -0
- package/dist/commands/lanes-checks.d.ts +3 -0
- package/dist/commands/lanes-checks.js +34 -0
- package/dist/commands/lanes-session-start.d.ts +17 -0
- package/dist/commands/lanes-session-start.js +195 -0
- package/dist/commands/lanes-shared.d.ts +2 -1
- package/dist/commands/lanes-watch.d.ts +2 -0
- package/dist/commands/lanes-watch.js +2 -1
- package/dist/commands/lanes.d.ts +3 -1
- package/dist/commands/lanes.js +121 -18
- package/dist/commands/members.d.ts +4 -0
- package/dist/commands/members.js +23 -2
- package/dist/lanes-drain-hook.js +6 -2
- package/dist/lib/daemon/conflict-notices.d.ts +4 -0
- package/dist/lib/daemon/conflict-notices.js +80 -0
- package/dist/lib/doctor/checks/sync-health.d.ts +2 -0
- package/dist/lib/doctor/checks/sync-health.js +47 -23
- package/dist/lib/import-scan/events.d.ts +135 -0
- package/dist/lib/import-scan/events.js +71 -0
- package/dist/lib/import-scan/run.d.ts +84 -0
- package/dist/lib/import-scan/run.js +308 -0
- package/dist/lib/import-scan/scan-event.schema.json +127 -0
- package/dist/lib/import-scan/validate.d.ts +31 -0
- package/dist/lib/import-scan/validate.js +111 -0
- package/dist/lib/lanes/dropbox.d.ts +4 -2
- package/dist/lib/lanes/dropbox.js +54 -9
- package/dist/lib/lanes/flag-keys.d.ts +10 -0
- package/dist/lib/lanes/flag-keys.js +10 -0
- package/dist/lib/lanes/hooks-install.d.ts +14 -11
- package/dist/lib/lanes/hooks-install.js +70 -50
- package/dist/lib/lanes/monitor.js +1 -1
- package/dist/lib/lanes/reaper.d.ts +58 -0
- package/dist/lib/lanes/reaper.js +403 -0
- package/dist/lib/lanes/registry-flag.d.ts +2 -0
- package/dist/lib/lanes/registry-flag.js +1 -1
- package/dist/lib/lanes/spawn.d.ts +2 -0
- package/dist/lib/lanes/spawn.js +32 -2
- package/dist/lib/lanes/status-fields.d.ts +12 -0
- package/dist/lib/lanes/status-fields.js +189 -0
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/lanes/worker-continuity.d.ts +18 -0
- package/dist/lib/lanes/worker-continuity.js +81 -0
- package/dist/lib/lanes/worker-process.d.ts +4 -0
- package/dist/lib/lanes/worker-process.js +77 -0
- package/dist/main.js +9 -3
- package/dist/utils/cli-telemetry.d.ts +4 -1
- package/dist/utils/cli-telemetry.js +8 -0
- package/dist/utils/jq-windows-compat.d.ts +13 -0
- package/dist/utils/jq-windows-compat.js +48 -0
- package/dist/utils/run-bundled-script.d.ts +3 -0
- package/dist/utils/run-bundled-script.js +9 -80
- package/dist/utils/windows-bash.d.ts +5 -0
- package/dist/utils/windows-bash.js +75 -0
- package/package.json +1 -1
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
7
|
+
import { readLanesRegistryFlagFailOpen } from "../lib/lanes/registry-flag.js";
|
|
8
|
+
import { LANE_CONDUCT_SESSION_START_FLAG } from "../lib/lanes/flag-keys.js";
|
|
9
|
+
function enabledFromSettings(root) {
|
|
10
|
+
const personal = path.join(root, "personal", "settings", "orchestrator.yaml");
|
|
11
|
+
const settings = fs.existsSync(personal) ? personal : path.join(root, "core", "settings", "orchestrator.yaml");
|
|
12
|
+
if (!fs.existsSync(settings))
|
|
13
|
+
return false;
|
|
14
|
+
const text = fs.readFileSync(settings, "utf8");
|
|
15
|
+
const block = /^conduct:\s*(?:#.*)?\r?\n((?:[ \t]+.*\r?\n?)*)/m.exec(text)?.[1] ?? "";
|
|
16
|
+
const value = /^\s+default_enabled:\s*["']?(true|false|yes|no|on|off|1|0)["']?\s*(?:#.*)?$/mi.exec(block)?.[1]?.toLowerCase();
|
|
17
|
+
return value === "true" || value === "yes" || value === "on" || value === "1";
|
|
18
|
+
}
|
|
19
|
+
function sessionWasDisabled(root, env, payloadSessionId) {
|
|
20
|
+
const sessionId = payloadSessionId || env.HQ_SESSION_ID;
|
|
21
|
+
if (!sessionId || !/^[A-Za-z0-9_-]+$/.test(sessionId))
|
|
22
|
+
return false;
|
|
23
|
+
const meta = path.join(root, "workspace", "sessions", sessionId, "meta.yaml");
|
|
24
|
+
if (!fs.existsSync(meta))
|
|
25
|
+
return false;
|
|
26
|
+
const text = fs.readFileSync(meta, "utf8");
|
|
27
|
+
return /^\s*(?:conduct_mode|conduct_enabled|conduct_default):\s*(?:off|false|no|0)\s*(?:#.*)?$/mi.test(text) ||
|
|
28
|
+
/^\s*conduct_engine:\s*(?:""|''|#.*)?\s*$/mi.test(text);
|
|
29
|
+
}
|
|
30
|
+
function extractConductorBlock(core) {
|
|
31
|
+
const start = core.indexOf("<!-- inject:start -->");
|
|
32
|
+
const end = core.indexOf("<!-- inject:end -->");
|
|
33
|
+
if (start < 0 || end < 0 || end <= start)
|
|
34
|
+
return null;
|
|
35
|
+
return core.slice(start + "<!-- inject:start -->".length, end).replace(/^\r?\n/, "").replace(/\r?\n$/, "");
|
|
36
|
+
}
|
|
37
|
+
const ORPHAN_SWEEP_LOCK_TTL_MS = 15 * 60_000;
|
|
38
|
+
/** Starts the CL-4 read-only orphan report once per host, without holding SessionStart open. */
|
|
39
|
+
export function startSessionStartOrphanSweep(root, env = process.env, spawnDetached = spawn) {
|
|
40
|
+
const stateRoot = env.XDG_STATE_HOME || path.join(os.homedir(), ".local", "state");
|
|
41
|
+
const host = os.hostname().replace(/[^A-Za-z0-9._-]/g, "_") || "unknown-host";
|
|
42
|
+
const stateDir = path.join(stateRoot, "hq-cli", "lanes-session-start", host);
|
|
43
|
+
const lockPath = path.join(stateDir, "orphan-sweep.lock");
|
|
44
|
+
const reportDir = path.join(stateRoot, "hq-cli", "lanes-orphan-reports", host);
|
|
45
|
+
const ownerToken = randomUUID();
|
|
46
|
+
try {
|
|
47
|
+
fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
48
|
+
fs.mkdirSync(reportDir, { recursive: true, mode: 0o700 });
|
|
49
|
+
let fd;
|
|
50
|
+
try {
|
|
51
|
+
fd = fs.openSync(lockPath, "wx", 0o600);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (error.code !== "EEXIST")
|
|
55
|
+
return;
|
|
56
|
+
let previous = {};
|
|
57
|
+
try {
|
|
58
|
+
previous = JSON.parse(fs.readFileSync(lockPath, "utf8"));
|
|
59
|
+
}
|
|
60
|
+
catch { /* malformed lease can be reclaimed */ }
|
|
61
|
+
const age = Date.now() - Number(previous.started_at || 0);
|
|
62
|
+
let running = false;
|
|
63
|
+
if (Number.isInteger(previous.pid) && Number(previous.pid) > 0) {
|
|
64
|
+
try {
|
|
65
|
+
process.kill(Number(previous.pid), 0);
|
|
66
|
+
running = true;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
running = false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (age < ORPHAN_SWEEP_LOCK_TTL_MS && running)
|
|
73
|
+
return;
|
|
74
|
+
fs.rmSync(lockPath, { force: true });
|
|
75
|
+
try {
|
|
76
|
+
fd = fs.openSync(lockPath, "wx", 0o600);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const startedAt = Date.now();
|
|
83
|
+
fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, started_at: startedAt, owner_token: ownerToken }));
|
|
84
|
+
fs.closeSync(fd);
|
|
85
|
+
const cli = process.argv[1];
|
|
86
|
+
if (!cli) {
|
|
87
|
+
fs.rmSync(lockPath, { force: true });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const wrapper = [
|
|
91
|
+
"const cp=require('node:child_process'),fs=require('node:fs'),path=require('node:path');",
|
|
92
|
+
"const [cli,lock,root,token,report]=process.argv.slice(1);",
|
|
93
|
+
"const cleanup=()=>{try{const lease=JSON.parse(fs.readFileSync(lock,'utf8'));if(lease.owner_token===token)fs.rmSync(lock,{force:true})}catch{}};",
|
|
94
|
+
"const start=()=>{let lease;try{lease=JSON.parse(fs.readFileSync(lock,'utf8'))}catch{}if(lease?.owner_token!==token){if(Date.now()<Number(lease?.started_at||0)+1000)return setTimeout(start,10);process.exit(0)}",
|
|
95
|
+
"try{fs.writeFileSync(lock,JSON.stringify({...lease,pid:process.pid}))}catch{}",
|
|
96
|
+
"const tmp=report+'.tmp';let output;try{output=fs.createWriteStream(tmp,{flags:'wx',mode:0o600})}catch{cleanup();process.exit(0)}",
|
|
97
|
+
"const child=cp.spawn(process.execPath,[cli,'lanes','_reap','--json'],{stdio:['ignore','pipe','ignore'],env:{...process.env,HQ_ROOT:root,HQ_NO_UPDATE_CHECK:'1'}});",
|
|
98
|
+
"child.stdout.pipe(output);child.on('error',()=>{output.destroy();try{fs.rmSync(tmp,{force:true})}catch{}cleanup()});",
|
|
99
|
+
"child.on('exit',(code)=>output.end(()=>{try{if(code===0)fs.renameSync(tmp,report);else fs.rmSync(tmp,{force:true})}catch{}cleanup()}));};start();",
|
|
100
|
+
].join("");
|
|
101
|
+
const reportPath = path.join(reportDir, `orphans-${startedAt}-${ownerToken}.json`);
|
|
102
|
+
const launcher = spawnDetached(process.execPath, ["-e", wrapper, cli, lockPath, root, ownerToken, reportPath], {
|
|
103
|
+
detached: true,
|
|
104
|
+
stdio: "ignore",
|
|
105
|
+
env: { ...env, HQ_ROOT: root, HQ_NO_UPDATE_CHECK: "1" },
|
|
106
|
+
});
|
|
107
|
+
if (launcher.pid) {
|
|
108
|
+
try {
|
|
109
|
+
fs.writeFileSync(lockPath, JSON.stringify({ pid: launcher.pid, started_at: startedAt, owner_token: ownerToken }));
|
|
110
|
+
}
|
|
111
|
+
catch { /* TTL lease still bounds an orphaned lock */ }
|
|
112
|
+
}
|
|
113
|
+
launcher.on("error", () => {
|
|
114
|
+
try {
|
|
115
|
+
const lease = JSON.parse(fs.readFileSync(lockPath, "utf8"));
|
|
116
|
+
if (lease.owner_token === ownerToken)
|
|
117
|
+
fs.rmSync(lockPath, { force: true });
|
|
118
|
+
}
|
|
119
|
+
catch { /* The lease may already have been reclaimed. */ }
|
|
120
|
+
});
|
|
121
|
+
launcher.unref();
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// SessionStart's orphan report is advisory and must never block startup.
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** SessionStart-compatible, fail-open conduct bootstrap. */
|
|
128
|
+
export async function runLaneSessionStart(options = {}) {
|
|
129
|
+
const env = options.env ?? process.env;
|
|
130
|
+
const stdin = options.stdin ?? process.stdin;
|
|
131
|
+
const stdout = options.stdout ?? process.stdout;
|
|
132
|
+
const stderr = options.stderr ?? process.stderr;
|
|
133
|
+
let debugLineWritten = false;
|
|
134
|
+
const debug = (message) => {
|
|
135
|
+
if (env.HQ_LANES_DEBUG === "1" && !debugLineWritten) {
|
|
136
|
+
debugLineWritten = true;
|
|
137
|
+
stderr.write(`${message}\n`);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
try {
|
|
141
|
+
const chunks = [];
|
|
142
|
+
for await (const chunk of stdin)
|
|
143
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
144
|
+
const input = Buffer.concat(chunks).toString("utf8");
|
|
145
|
+
let source = "startup";
|
|
146
|
+
let payloadSessionId;
|
|
147
|
+
try {
|
|
148
|
+
const payload = JSON.parse(input);
|
|
149
|
+
source = payload.source || "startup";
|
|
150
|
+
payloadSessionId = payload.session_id;
|
|
151
|
+
}
|
|
152
|
+
catch { /* malformed hook input uses startup default */ }
|
|
153
|
+
if (source !== "startup")
|
|
154
|
+
return;
|
|
155
|
+
const disabledHooks = `,${env.HQ_DISABLED_HOOKS ?? ""},`.replace(/\s/g, "");
|
|
156
|
+
if (/(^|,)auto-conduct(,|$)/.test(disabledHooks))
|
|
157
|
+
return;
|
|
158
|
+
const root = (options.resolveRoot ?? ((e, cwd) => resolveLanesHqRoot({ env: e, cwd })))(env, options.cwd);
|
|
159
|
+
const flagEnabled = await readLanesRegistryFlagFailOpen(LANE_CONDUCT_SESSION_START_FLAG, {
|
|
160
|
+
env,
|
|
161
|
+
defaultValue: true,
|
|
162
|
+
reportFailure: (error) => {
|
|
163
|
+
const kind = error instanceof Error ? error.name : "Error";
|
|
164
|
+
debug(`hq lanes session-start: flag read failed (${kind})`);
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
if (!flagEnabled)
|
|
168
|
+
return;
|
|
169
|
+
(options.startSweep ?? ((r, e) => startSessionStartOrphanSweep(r, e, spawn)))(root, env);
|
|
170
|
+
if (sessionWasDisabled(root, env, payloadSessionId))
|
|
171
|
+
return;
|
|
172
|
+
let enabled = enabledFromSettings(root);
|
|
173
|
+
if (/^(1|true|on|yes)$/i.test(env.HQ_AUTO_CONDUCT ?? ""))
|
|
174
|
+
enabled = true;
|
|
175
|
+
else if (/^(0|false|off|no)$/i.test(env.HQ_AUTO_CONDUCT ?? ""))
|
|
176
|
+
enabled = false;
|
|
177
|
+
if (!enabled)
|
|
178
|
+
return;
|
|
179
|
+
const core = path.join(root, ".claude", "skills", "conduct", "conductor-core.md");
|
|
180
|
+
if (!fs.existsSync(core))
|
|
181
|
+
return;
|
|
182
|
+
const block = extractConductorBlock(fs.readFileSync(core, "utf8"));
|
|
183
|
+
if (block)
|
|
184
|
+
stdout.write(`${block}\n`);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
debug("hq lanes session-start: bootstrap failed");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
export function registerLaneSessionStartCommand(lanes) {
|
|
191
|
+
lanes.command("session-start", { hidden: true })
|
|
192
|
+
.description("Internal SessionStart bootstrap")
|
|
193
|
+
.action(async () => runLaneSessionStart());
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=lanes-session-start.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LaneCompanyRef, LanePlacementPolicy, LaneRecord, LanesConfig, QuestionAnswer, QuestionBody, QuestionRecord, SeniorRef, AccountHistoryEntry, LaneAccountSelection, PendingResume, QueuedReason, ResolvedLaneWorker, LivenessDeps, MachineIdentity, MonitorEvidenceReader, ReconcileHost, RunCommand, ThrottleDeps, withLaneHireLocks } from "../lib/lanes/index.js";
|
|
1
|
+
import type { LaneCompanyRef, LanePlacementPolicy, LaneRecord, LanesConfig, QuestionAnswer, QuestionBody, QuestionRecord, SeniorRef, AccountHistoryEntry, LaneAccountSelection, PendingResume, QueuedReason, ResolvedLaneWorker, LivenessDeps, MachineIdentity, MonitorEvidenceReader, ReconcileHost, RunCommand, ThrottleDeps, withLaneAdmissionLock, withLaneHireLocks } from "../lib/lanes/index.js";
|
|
2
2
|
import type { OutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
3
3
|
import type { OutpostDispatchResult } from "../lib/lanes/outpost-dispatch.js";
|
|
4
4
|
import type { CpuBusySource } from "../lib/lanes/throttle.js";
|
|
@@ -313,6 +313,7 @@ export interface Ctx {
|
|
|
313
313
|
storyLedgerEnabled: () => boolean;
|
|
314
314
|
isSsmOwnerDispatch: () => boolean;
|
|
315
315
|
withGrantHireLocks: typeof withLaneHireLocks;
|
|
316
|
+
withAdmissionLock: typeof withLaneAdmissionLock;
|
|
316
317
|
mergeHoldEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
317
318
|
mergeReviewEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
318
319
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { type MonitorEngine } from "../lib/lanes/index.js";
|
|
1
2
|
import type { Ctx, MonitorCheckOptions, LaneWaitCommandOptions, LaneWatchHandlerDeps, LaneWatchOptions } from "./lanes-shared.js";
|
|
3
|
+
export declare function resolveMonitorEngine(requested: string | undefined, env: NodeJS.ProcessEnv): MonitorEngine;
|
|
2
4
|
export declare function createLaneWatchHandlers(deps: LaneWatchHandlerDeps): {
|
|
3
5
|
runWatch: (ctx: Ctx, laneIds: string[], opts: LaneWatchOptions) => Promise<number>;
|
|
4
6
|
runMonitorCheck: (ctx: Ctx, opts: MonitorCheckOptions) => Promise<number>;
|
|
@@ -10,8 +10,9 @@ const MONITOR_ACTIVE_LANE_STATES = new Set([
|
|
|
10
10
|
"awaiting_input",
|
|
11
11
|
"blocked",
|
|
12
12
|
]);
|
|
13
|
-
function resolveMonitorEngine(requested, env) {
|
|
13
|
+
export function resolveMonitorEngine(requested, env) {
|
|
14
14
|
const value = requested?.trim().toLowerCase() ||
|
|
15
|
+
env.HQ_LANE_ENGINE?.trim().toLowerCase() ||
|
|
15
16
|
env.HQ_CONDUCT_ENGINE?.trim().toLowerCase() ||
|
|
16
17
|
"claude";
|
|
17
18
|
if (!MONITOR_ENGINES.includes(value)) {
|
package/dist/commands/lanes.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/
|
|
|
17
17
|
export type { OutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
18
18
|
import { type OutpostDispatchResult } from "../lib/lanes/outpost-dispatch.js";
|
|
19
19
|
import { emitLaneDecisionEvents } from "../lib/lanes/mesh-events.js";
|
|
20
|
-
import { MAX_LANE_DEPTH, withLaneHireLocks, type LaneCompanyRef, type QuestionBody, type LaneRecord, type QuestionRecord, type ResolvedLaneWorker, type SeniorRef, type LivenessDeps, type MachineIdentity, type MonitorEvidenceReader, type RunCommand, type ThrottleDeps } from "../lib/lanes/index.js";
|
|
20
|
+
import { MAX_LANE_DEPTH, withLaneAdmissionLock, withLaneHireLocks, type LaneCompanyRef, type QuestionBody, type LaneRecord, type QuestionRecord, type ResolvedLaneWorker, type SeniorRef, type LivenessDeps, type MachineIdentity, type MonitorEvidenceReader, type RunCommand, type ThrottleDeps } from "../lib/lanes/index.js";
|
|
21
21
|
import { questionBodyAbsPath } from "../lib/lanes/questions.js";
|
|
22
22
|
import { type RunCompactionFn } from "../lib/lanes/compaction.js";
|
|
23
23
|
import type { WatcherLoopDeps } from "../lib/lanes/watcher.js";
|
|
@@ -106,6 +106,8 @@ export interface LanesCommandDeps {
|
|
|
106
106
|
isSsmOwnerDispatch?: () => boolean;
|
|
107
107
|
/** Test seam for changing a lane between pre-lock and locked grant reads. */
|
|
108
108
|
withGrantHireLocks?: typeof withLaneHireLocks;
|
|
109
|
+
/** Test seam proving create admission encloses the worker busy check. */
|
|
110
|
+
withAdmissionLock?: typeof withLaneAdmissionLock;
|
|
109
111
|
/** Test seam for the review-before-merge registry flag. */
|
|
110
112
|
mergeReviewEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
111
113
|
/** Test seam for the repository-merge-hold registry flag. */
|
package/dist/commands/lanes.js
CHANGED
|
@@ -33,13 +33,16 @@ import { registerLaneReviewCommand } from "./lanes-review.js";
|
|
|
33
33
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
34
34
|
import { withCliFeatureUsed } from "../utils/cli-telemetry.js";
|
|
35
35
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
36
|
+
import { registerLaneSessionStartCommand } from "./lanes-session-start.js";
|
|
36
37
|
import { laneDepthCap } from "../lib/lanes/depth-cap.js";
|
|
37
38
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
38
39
|
import { runResidentCommandWithPackageLease } from "../utils/resident-package-preload.js";
|
|
39
40
|
import { readLanesFlagLocalOverride, readLanesRegistryFlag, readLanesRegistryFlagFailOff, readLanesRegistryFlagFailOpen } from "../lib/lanes/registry-flag.js";
|
|
41
|
+
import { readLaneStatusRowFields } from "../lib/lanes/status-fields.js";
|
|
40
42
|
import { appendProjectFocusOverride, readProjectStoryFocusData, removeProjectFocusOverride } from "../lib/lanes/story-ledger.js";
|
|
41
43
|
import { appendLaneFocusOverride, assertFocusGuardAllows, makeFocusOverride } from "../lib/lanes/focus-guard.js";
|
|
42
|
-
import { LANE_FOCUS_GUARD_FLAG, LANE_FOCUS_DASHBOARD_FLAG, LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG, LANE_REUSE_REFRESHES_PROFILE_FLAG, } from "../lib/lanes/flag-keys.js";
|
|
44
|
+
import { LANE_FOCUS_GUARD_FLAG, LANE_FOCUS_DASHBOARD_FLAG, LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG, LANE_STATUS_ROW_FIELDS_FLAG, LANE_REAP_STOP_FLAG, LANE_REUSE_REFRESHES_PROFILE_FLAG, LANE_WORKER_CONTINUITY_FLAG, } from "../lib/lanes/flag-keys.js";
|
|
45
|
+
import { findWorkerContinuityLane, sameLaneHost, workerContinuityPrompt } from "../lib/lanes/worker-continuity.js";
|
|
43
46
|
import { configuredCodexRolloutHomes, retainCodexRollouts } from "../lib/lanes/disk-guard.js";
|
|
44
47
|
import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
|
|
45
48
|
import { runLeadAutopilotPass } from "../lib/lanes/autopilot.js";
|
|
@@ -461,6 +464,7 @@ function resolveDeps(deps = {}) {
|
|
|
461
464
|
focusGuardEnabled: deps.focusGuardEnabled,
|
|
462
465
|
isSsmOwnerDispatch: deps.isSsmOwnerDispatch ?? (() => isSsmRunCommandProcess()),
|
|
463
466
|
withGrantHireLocks: deps.withGrantHireLocks ?? withLaneHireLocks,
|
|
467
|
+
withAdmissionLock: deps.withAdmissionLock ?? withLaneAdmissionLock,
|
|
464
468
|
mergeReviewEnabled: deps.mergeReviewEnabled,
|
|
465
469
|
mergeHoldEnabled: deps.mergeHoldEnabled,
|
|
466
470
|
};
|
|
@@ -3364,6 +3368,16 @@ async function runCreate(ctx, opts) {
|
|
|
3364
3368
|
const provider = explicitProvider ??
|
|
3365
3369
|
profile?.meta.provider ??
|
|
3366
3370
|
parseLaneProvider("codex");
|
|
3371
|
+
const continuityEnabled = opts.worker !== undefined && await readLanesRegistryFlagFailOpen(LANE_WORKER_CONTINUITY_FLAG, { company: company ?? meshCompany, env: ctx.env });
|
|
3372
|
+
let workerContinuity = continuityEnabled
|
|
3373
|
+
? findWorkerContinuityLane(loadAllLanes(hqRoot), {
|
|
3374
|
+
senior,
|
|
3375
|
+
worker: resolvedWorker?.id ?? opts.worker,
|
|
3376
|
+
provider,
|
|
3377
|
+
company: company ?? meshCompany,
|
|
3378
|
+
freshThread: opts.freshThread,
|
|
3379
|
+
})
|
|
3380
|
+
: { resumable: false };
|
|
3367
3381
|
const profileDefaultsApply = profile !== undefined && profile.meta.provider === provider;
|
|
3368
3382
|
const cliModel = parseCliModel(opts.model);
|
|
3369
3383
|
// Validated here because `claude --effort <unknown>` only warns and runs.
|
|
@@ -3738,6 +3752,24 @@ async function runCreate(ctx, opts) {
|
|
|
3738
3752
|
// The lane id is created in `persist`; its Work Mesh thread is ensured
|
|
3739
3753
|
// immediately afterward from that stable identity.
|
|
3740
3754
|
const persist = async () => {
|
|
3755
|
+
const host = placementHostOverride ?? (outpostId !== undefined
|
|
3756
|
+
? { kind: "outpost", id: outpostId }
|
|
3757
|
+
: { kind: "local", id: ctx.hostname() });
|
|
3758
|
+
// Repeat the busy check while holding provider admission serialization so
|
|
3759
|
+
// two concurrent creates cannot both claim one worker's provider thread.
|
|
3760
|
+
if (continuityEnabled) {
|
|
3761
|
+
workerContinuity = findWorkerContinuityLane(loadAllLanes(hqRoot), {
|
|
3762
|
+
senior,
|
|
3763
|
+
worker: resolvedWorker?.id ?? opts.worker,
|
|
3764
|
+
provider,
|
|
3765
|
+
company: company ?? meshCompany,
|
|
3766
|
+
freshThread: opts.freshThread,
|
|
3767
|
+
});
|
|
3768
|
+
if (workerContinuity.resumable && workerContinuity.previous &&
|
|
3769
|
+
!sameLaneHost(workerContinuity.previous.host, host)) {
|
|
3770
|
+
workerContinuity = { previous: workerContinuity.previous, resumable: false, fallbackReason: "host_mismatch" };
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3741
3773
|
let admission;
|
|
3742
3774
|
let validation;
|
|
3743
3775
|
if (!outpostId && provider === "claude" && explicitBalanceAccount === undefined &&
|
|
@@ -3841,9 +3873,6 @@ async function runCreate(ctx, opts) {
|
|
|
3841
3873
|
throw admission.error;
|
|
3842
3874
|
}
|
|
3843
3875
|
}
|
|
3844
|
-
const host = placementHostOverride ?? (outpostId !== undefined
|
|
3845
|
-
? { kind: "outpost", id: outpostId }
|
|
3846
|
-
: { kind: "local", id: ctx.hostname() });
|
|
3847
3876
|
const queuedReason = admission.action === "queue" ? admission.reason : undefined;
|
|
3848
3877
|
const admissionReservedAt = admission.action === "start" ? ctx.now().toISOString() : undefined;
|
|
3849
3878
|
const laneInit = {
|
|
@@ -3887,6 +3916,18 @@ async function runCreate(ctx, opts) {
|
|
|
3887
3916
|
keep_alive: keepAlive,
|
|
3888
3917
|
...(keepAlive ? { keep_alive_opted_in_at: createdAt } : {}),
|
|
3889
3918
|
provider,
|
|
3919
|
+
...(workerContinuity.resumable && workerContinuity.previous
|
|
3920
|
+
? {
|
|
3921
|
+
provider_session_id: workerContinuity.previous.provider_session_id,
|
|
3922
|
+
last_message_at: workerContinuity.previous.last_message_at ?? workerContinuity.previous.created_at,
|
|
3923
|
+
pending_resume: {
|
|
3924
|
+
session_id: workerContinuity.previous.provider_session_id,
|
|
3925
|
+
at: createdAt,
|
|
3926
|
+
trigger: "resume",
|
|
3927
|
+
resumed_from: workerContinuity.previous.state,
|
|
3928
|
+
},
|
|
3929
|
+
}
|
|
3930
|
+
: {}),
|
|
3890
3931
|
provider_source: explicitProvider !== undefined
|
|
3891
3932
|
? "cli"
|
|
3892
3933
|
: profile?.meta.provider !== undefined
|
|
@@ -3956,12 +3997,7 @@ async function runCreate(ctx, opts) {
|
|
|
3956
3997
|
return { lane, admission, validation };
|
|
3957
3998
|
};
|
|
3958
3999
|
let persisted;
|
|
3959
|
-
|
|
3960
|
-
persisted = await persist();
|
|
3961
|
-
}
|
|
3962
|
-
else {
|
|
3963
|
-
persisted = await withLaneAdmissionLock(hqRoot, provider, persist, ctx.sleep);
|
|
3964
|
-
}
|
|
4000
|
+
persisted = await ctx.withAdmissionLock(hqRoot, provider, persist, ctx.sleep);
|
|
3965
4001
|
let lane = persisted.lane;
|
|
3966
4002
|
const { admission, validation } = persisted;
|
|
3967
4003
|
if (focusAssessment.enabled && opts.focusOverride !== undefined) {
|
|
@@ -4051,10 +4087,25 @@ async function runCreate(ctx, opts) {
|
|
|
4051
4087
|
ensureDir(path.join(dest, "dropbox"));
|
|
4052
4088
|
ensureDir(path.join(dest, "questions"));
|
|
4053
4089
|
const taskBrief = fs.readFileSync(briefAbs, "utf8");
|
|
4054
|
-
const
|
|
4090
|
+
const previousResult = workerContinuity.previous
|
|
4091
|
+
? readLatestEnvelopeRecord(hqRoot, workerContinuity.previous.lane_id)
|
|
4092
|
+
: undefined;
|
|
4093
|
+
const briefText = `${workerContinuity.resumable && workerContinuity.previous
|
|
4094
|
+
? workerContinuityPrompt(workerContinuity.previous, previousResult?.envelope.summary, previousResult?.ref)
|
|
4095
|
+
: ""}${resolvedWorker
|
|
4055
4096
|
? composeWorkerBrief(resolvedWorker, taskBrief)
|
|
4056
|
-
: composeGeneralBrief(taskBrief)
|
|
4097
|
+
: composeGeneralBrief(taskBrief)}`;
|
|
4057
4098
|
fs.writeFileSync(path.join(dest, "brief.md"), briefText);
|
|
4099
|
+
if (workerContinuity.resumable && lane.pending_resume) {
|
|
4100
|
+
const newBriefPath = path.join(dest, "brief.md");
|
|
4101
|
+
lane = casUpdateLane(hqRoot, lane.lane_id, (current) => ({
|
|
4102
|
+
...current,
|
|
4103
|
+
pending_resume: current.pending_resume ? {
|
|
4104
|
+
...current.pending_resume,
|
|
4105
|
+
text: `The new brief at ${newBriefPath} is the instruction for this turn. Read and follow it.`,
|
|
4106
|
+
} : undefined,
|
|
4107
|
+
}));
|
|
4108
|
+
}
|
|
4058
4109
|
if (timeoutSecs !== undefined) {
|
|
4059
4110
|
writeJsonFile(timeoutPath(hqRoot, lane.lane_id), {
|
|
4060
4111
|
timeout_secs: timeoutSecs,
|
|
@@ -4121,6 +4172,16 @@ async function runCreate(ctx, opts) {
|
|
|
4121
4172
|
stories: laneStories(started),
|
|
4122
4173
|
story_warning: combinedStoryWarning(started, createStoryWarnings),
|
|
4123
4174
|
thread_id: started.thread_id ?? null,
|
|
4175
|
+
...(continuityEnabled ? {
|
|
4176
|
+
worker_continuity: workerContinuity.resumable
|
|
4177
|
+
? { action: "resume", previous_lane_id: workerContinuity.previous?.lane_id }
|
|
4178
|
+
: {
|
|
4179
|
+
action: "fresh",
|
|
4180
|
+
...(workerContinuity.previous ? { previous_lane_id: workerContinuity.previous.lane_id } : {}),
|
|
4181
|
+
...(opts.freshThread ? { reason: "fresh_thread_requested" } : {}),
|
|
4182
|
+
...(workerContinuity.fallbackReason ? { reason: workerContinuity.fallbackReason } : {}),
|
|
4183
|
+
},
|
|
4184
|
+
} : {}),
|
|
4124
4185
|
senior: started.senior,
|
|
4125
4186
|
depth: started.depth,
|
|
4126
4187
|
timeout_secs: timeoutSecs ?? null,
|
|
@@ -5342,6 +5403,24 @@ async function runPickup(ctx, opts) {
|
|
|
5342
5403
|
lane: started,
|
|
5343
5404
|
});
|
|
5344
5405
|
}
|
|
5406
|
+
async function laneStatusFieldsForCommand(ctx, hqRoot, lane, now) {
|
|
5407
|
+
const resolved = resolveLaneCompany(hqRoot, lane).company;
|
|
5408
|
+
let enabled = true;
|
|
5409
|
+
try {
|
|
5410
|
+
enabled = await readLanesRegistryFlagFailOpen(LANE_STATUS_ROW_FIELDS_FLAG, {
|
|
5411
|
+
...(resolved ? { company: resolved } : {}),
|
|
5412
|
+
env: ctx.env,
|
|
5413
|
+
});
|
|
5414
|
+
}
|
|
5415
|
+
catch (err) {
|
|
5416
|
+
process.stderr.write(`hq lanes: status row fields flag read failed open: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
5417
|
+
}
|
|
5418
|
+
if (!enabled)
|
|
5419
|
+
return {};
|
|
5420
|
+
const host = reconcileHostFromCtx(ctx);
|
|
5421
|
+
const local = laneRunsHere(lane, host);
|
|
5422
|
+
return { ...readLaneStatusRowFields(hqRoot, lane, now.getTime(), local) };
|
|
5423
|
+
}
|
|
5345
5424
|
async function runList(ctx, opts) {
|
|
5346
5425
|
if (opts.json && opts.human) {
|
|
5347
5426
|
throw new LanesError("conflicting_output_format", "Use either --json or --human for hq lanes list", { json: true, human: true });
|
|
@@ -5364,7 +5443,7 @@ async function runList(ctx, opts) {
|
|
|
5364
5443
|
new Set(opts.hierarchy).size !== opts.hierarchy.length)) {
|
|
5365
5444
|
throw new LanesError("lane_list_hierarchy_invalid", "--hierarchy must be repeated with distinct, non-empty lane ids without surrounding whitespace", {});
|
|
5366
5445
|
}
|
|
5367
|
-
if (opts.snapshot && (opts.human || opts.company || opts.project || opts.story || opts.state || opts.senior)) {
|
|
5446
|
+
if (opts.snapshot && (opts.human || opts.company || opts.project || opts.story || opts.state || opts.senior || opts.worker)) {
|
|
5368
5447
|
throw new LanesError("lane_list_snapshot_invalid", "--snapshot only supports owner lane filters and JSON output", {});
|
|
5369
5448
|
}
|
|
5370
5449
|
if (opts.hierarchy !== undefined && !opts.snapshot) {
|
|
@@ -5413,21 +5492,29 @@ async function runList(ctx, opts) {
|
|
|
5413
5492
|
}
|
|
5414
5493
|
if (senior && seniorKey(lane.senior) !== seniorKey(senior))
|
|
5415
5494
|
return false;
|
|
5495
|
+
if (opts.worker && lane.worker !== opts.worker)
|
|
5496
|
+
return false;
|
|
5416
5497
|
return true;
|
|
5417
5498
|
};
|
|
5418
5499
|
const selectLanes = () => laneSource
|
|
5419
5500
|
.filter((lane) => matchesLaneFilters(lane, true))
|
|
5420
5501
|
.sort(compareLaneQueueOrder);
|
|
5421
5502
|
let selectedLanes = selectLanes();
|
|
5503
|
+
const statusFieldsByLane = new Map();
|
|
5504
|
+
const statusNow = ctx.now();
|
|
5505
|
+
await Promise.all(selectedLanes.map(async (lane) => {
|
|
5506
|
+
statusFieldsByLane.set(lane.lane_id, await laneStatusFieldsForCommand(ctx, hqRoot, lane, statusNow));
|
|
5507
|
+
}));
|
|
5422
5508
|
if (opts.snapshot) {
|
|
5423
5509
|
printJson({
|
|
5424
5510
|
ok: true,
|
|
5425
5511
|
action: "list",
|
|
5426
5512
|
lanes: selectedLanes.map((lane) => {
|
|
5513
|
+
const fields = statusFieldsByLane.get(lane.lane_id) ?? {};
|
|
5427
5514
|
if (!isTerminalLaneState(lane.state))
|
|
5428
|
-
return lane;
|
|
5515
|
+
return { ...lane, ...fields };
|
|
5429
5516
|
const envelope = readLatestEnvelopeRecord(hqRoot, lane.lane_id)?.envelope;
|
|
5430
|
-
return envelope?.summary ? {
|
|
5517
|
+
return { ...lane, ...fields, ...(envelope?.summary ? { envelope_summary: envelope.summary } : {}) };
|
|
5431
5518
|
}),
|
|
5432
5519
|
});
|
|
5433
5520
|
return;
|
|
@@ -5531,6 +5618,7 @@ async function runList(ctx, opts) {
|
|
|
5531
5618
|
!laneRunsHere(lane, host);
|
|
5532
5619
|
return {
|
|
5533
5620
|
...lane,
|
|
5621
|
+
...statusFieldsByLane.get(lane.lane_id),
|
|
5534
5622
|
...hireByLane.get(lane.lane_id),
|
|
5535
5623
|
...(remoteRefresh.unverifiedRemote.has(lane.lane_id) ? { unverified_remote: true } : {}),
|
|
5536
5624
|
stories: laneStories(lane),
|
|
@@ -5900,7 +5988,10 @@ async function runShow(ctx, laneId, hasPendingQuestionForLane, resolveLaneTarget
|
|
|
5900
5988
|
})
|
|
5901
5989
|
.slice(0, 20)
|
|
5902
5990
|
.map(({ repo, pr, head_sha, recorded_at }) => ({ repo, pr, head_sha, recorded_at }));
|
|
5903
|
-
const laneForShow = {
|
|
5991
|
+
const laneForShow = {
|
|
5992
|
+
...lane,
|
|
5993
|
+
...await laneStatusFieldsForCommand(ctx, hqRoot, storedLane, ctx.now()),
|
|
5994
|
+
};
|
|
5904
5995
|
delete laneForShow.resume_history;
|
|
5905
5996
|
printJson({
|
|
5906
5997
|
ok: true,
|
|
@@ -9234,8 +9325,9 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9234
9325
|
timeoutPath,
|
|
9235
9326
|
});
|
|
9236
9327
|
const lanes = parent
|
|
9237
|
-
.command("lanes"
|
|
9328
|
+
.command("lanes")
|
|
9238
9329
|
.description("Create, supervise, and steer worker lanes; routed decision queue");
|
|
9330
|
+
registerLaneSessionStartCommand(lanes);
|
|
9239
9331
|
registerLaneChecksCommand(lanes, {
|
|
9240
9332
|
hqRoot: ctx.hqRoot,
|
|
9241
9333
|
resolveCompany: ctx.resolveCompany,
|
|
@@ -9246,6 +9338,15 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9246
9338
|
presenceConnected: ctx.presenceConnected ?? false,
|
|
9247
9339
|
deps: ctx.livenessDeps,
|
|
9248
9340
|
}),
|
|
9341
|
+
stopEnabled: (lane) => {
|
|
9342
|
+
const resolution = resolveLaneCompany(ctx.hqRoot(), lane);
|
|
9343
|
+
const company = resolution.company ?? undefined;
|
|
9344
|
+
return readLanesRegistryFlag(LANE_REAP_STOP_FLAG, {
|
|
9345
|
+
company,
|
|
9346
|
+
...(company ? { companyUidSource: laneCompanyUidSource(resolution.source) } : {}),
|
|
9347
|
+
env: ctx.env,
|
|
9348
|
+
});
|
|
9349
|
+
},
|
|
9249
9350
|
}, wrap);
|
|
9250
9351
|
registerLaneDropboxRepairCommand(lanes, {
|
|
9251
9352
|
hqRoot: ctx.hqRoot,
|
|
@@ -9311,6 +9412,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9311
9412
|
.option("--additional-story <id>", "Additional story carried by this lane; repeatable and discouraged when active", collectRepeatable)
|
|
9312
9413
|
.requiredOption("--brief-file <path>", "Path to the lane brief")
|
|
9313
9414
|
.option("--worker <name>", "HQ worker or reusable profile id")
|
|
9415
|
+
.option("--fresh-thread", "Skip worker continuity and start a cold provider session")
|
|
9314
9416
|
.option("--provider <name>", "claude | codex | grok")
|
|
9315
9417
|
.option("--account <id>", "Provider account id")
|
|
9316
9418
|
.option("--model <id>", "Model id for the provider CLI")
|
|
@@ -9410,6 +9512,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9410
9512
|
.option("--story <id>", "Filter by story")
|
|
9411
9513
|
.option("--state <state>", `Filter by state (${LANE_STATES.join("|")})`)
|
|
9412
9514
|
.option("--senior <ref>", "Filter by senior ref")
|
|
9515
|
+
.option("--worker <id>", "Filter by worker id")
|
|
9413
9516
|
.option("--lane <id>", "Filter by lane id (repeatable)", (value, previous = []) => [...previous, value], [])
|
|
9414
9517
|
.addOption(new Option("--lane-ulid <ulid>", "Filter by lane ULID for internal owner lookup").hideHelp()
|
|
9415
9518
|
.argParser((value, previous = []) => [...previous, value]))
|
|
@@ -9506,7 +9609,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9506
9609
|
.command("monitor-check")
|
|
9507
9610
|
.description("Check that active Claude senior lanes have live Monitor watch tasks")
|
|
9508
9611
|
.option("--session <id>", "Senior session id")
|
|
9509
|
-
.option("--engine <engine>", "claude | codex | grok; defaults to HQ_CONDUCT_ENGINE or claude")
|
|
9612
|
+
.option("--engine <engine>", "claude | codex | grok; defaults to HQ_LANE_ENGINE (HQ_CONDUCT_ENGINE is deprecated) or claude")
|
|
9510
9613
|
.option("--reminder", "Print the senior Monitor reminder text")
|
|
9511
9614
|
.option("--json", "JSON on stdout")
|
|
9512
9615
|
.action(wrapText(async (opts) => watchHandlers.runMonitorCheck(ctx, opts)));
|
|
@@ -48,6 +48,8 @@ export interface InviteOptions {
|
|
|
48
48
|
companyUid: string;
|
|
49
49
|
callerUid: string;
|
|
50
50
|
token: string;
|
|
51
|
+
/** Reports only whether the request returned a 2xx response. */
|
|
52
|
+
onRequestResult?: (ok: boolean) => void;
|
|
51
53
|
}
|
|
52
54
|
/**
|
|
53
55
|
* Outcome of `hq members invite`. Two shapes depending on server schema:
|
|
@@ -96,6 +98,8 @@ export interface ResendInviteOptions {
|
|
|
96
98
|
companyUid: string;
|
|
97
99
|
callerUid: string;
|
|
98
100
|
token: string;
|
|
101
|
+
/** Reports only whether the request returned a 2xx response. */
|
|
102
|
+
onRequestResult?: (ok: boolean) => void;
|
|
99
103
|
}
|
|
100
104
|
/**
|
|
101
105
|
* Outcome of `hq members invite --resend`. No new membership row was
|
package/dist/commands/members.js
CHANGED
|
@@ -3,6 +3,7 @@ import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
|
|
|
3
3
|
import { gateApiKeyCapabilities } from "../utils/api-key-command-gate.js";
|
|
4
4
|
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
5
5
|
import { isPlanGateError } from "../utils/plan-gate-error.js";
|
|
6
|
+
import { queueCliFeatureUsed } from "../utils/cli-telemetry.js";
|
|
6
7
|
const EMAIL_PATTERN = /^[^\s]+@[^\s]+$/;
|
|
7
8
|
const PERSON_UID_PATTERN = /^prs_[A-Za-z0-9_-]+$/;
|
|
8
9
|
const AGENT_UID_PATTERN = /^agt_[A-Za-z0-9_-]+$/i;
|
|
@@ -107,6 +108,7 @@ export async function inviteMember(options) {
|
|
|
107
108
|
body,
|
|
108
109
|
});
|
|
109
110
|
if (!res.ok) {
|
|
111
|
+
options.onRequestResult?.(false);
|
|
110
112
|
const err = (await res.json().catch(() => ({})));
|
|
111
113
|
throw new InviteHttpError(res.status, err.message ?? err.error ?? res.statusText, err.code);
|
|
112
114
|
}
|
|
@@ -116,6 +118,7 @@ export async function inviteMember(options) {
|
|
|
116
118
|
throw new Error(`Invite endpoint returned 2xx with no membership row (response keys: ${keys}). ` +
|
|
117
119
|
"This is a server-side regression — file an issue.");
|
|
118
120
|
}
|
|
121
|
+
options.onRequestResult?.(true);
|
|
119
122
|
// Two server schemas in the wild:
|
|
120
123
|
// - Legacy (schemaVersion ≤ 1): response carries a random `inviteToken`
|
|
121
124
|
// the invitee redeems via `hq://accept/{token}`.
|
|
@@ -177,6 +180,7 @@ export async function resendInvite(options) {
|
|
|
177
180
|
},
|
|
178
181
|
});
|
|
179
182
|
if (!res.ok) {
|
|
183
|
+
options.onRequestResult?.(false);
|
|
180
184
|
const err = (await res.json().catch(() => ({})));
|
|
181
185
|
throw new InviteHttpError(res.status, err.message ?? err.error ?? res.statusText, err.code);
|
|
182
186
|
}
|
|
@@ -184,6 +188,7 @@ export async function resendInvite(options) {
|
|
|
184
188
|
if (data.resent !== true) {
|
|
185
189
|
throw new Error("Resend endpoint returned 2xx without `resent: true` — the connected hq-pro likely doesn't support `resend: true` yet. Upgrade hq-pro or re-invite without --resend.");
|
|
186
190
|
}
|
|
191
|
+
options.onRequestResult?.(true);
|
|
187
192
|
return {
|
|
188
193
|
resent: true,
|
|
189
194
|
emailSent: data.emailSent === true,
|
|
@@ -436,6 +441,7 @@ export function registerMembersCommand(program) {
|
|
|
436
441
|
let token;
|
|
437
442
|
let companyUid;
|
|
438
443
|
let companySlug;
|
|
444
|
+
let outcome = "failure";
|
|
439
445
|
try {
|
|
440
446
|
token = (await resolveVaultCredential()).token;
|
|
441
447
|
companySlug = members.opts().company;
|
|
@@ -448,6 +454,9 @@ export function registerMembersCommand(program) {
|
|
|
448
454
|
companyUid,
|
|
449
455
|
callerUid,
|
|
450
456
|
token,
|
|
457
|
+
onRequestResult: (ok) => {
|
|
458
|
+
outcome = ok ? "success" : "failure";
|
|
459
|
+
},
|
|
451
460
|
});
|
|
452
461
|
console.log(chalk.green(`Re-fired invite email for ${target.toLowerCase()} (existing pending row left untouched)`));
|
|
453
462
|
console.log();
|
|
@@ -472,6 +481,9 @@ export function registerMembersCommand(program) {
|
|
|
472
481
|
companyUid,
|
|
473
482
|
callerUid,
|
|
474
483
|
token,
|
|
484
|
+
onRequestResult: (ok) => {
|
|
485
|
+
outcome = ok ? "success" : "failure";
|
|
486
|
+
},
|
|
475
487
|
});
|
|
476
488
|
const agentTarget = detectTarget(target)?.isAgent === true;
|
|
477
489
|
console.log(chalk.green(`Invited ${target} as ${result.membership.role} (status: ${result.membership.status})`));
|
|
@@ -612,10 +624,19 @@ export function registerMembersCommand(program) {
|
|
|
612
624
|
}
|
|
613
625
|
if (err instanceof InviteHttpError) {
|
|
614
626
|
console.error(chalk.red(formatInviteHttpError(err.status, err.message, err.code)));
|
|
615
|
-
process.
|
|
627
|
+
process.exitCode = 1;
|
|
628
|
+
return;
|
|
616
629
|
}
|
|
617
630
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
618
|
-
process.
|
|
631
|
+
process.exitCode = 1;
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
finally {
|
|
635
|
+
queueCliFeatureUsed({
|
|
636
|
+
kind: "members_invite",
|
|
637
|
+
outcome,
|
|
638
|
+
...(companyUid ? { companyUid } : {}),
|
|
639
|
+
});
|
|
619
640
|
}
|
|
620
641
|
});
|
|
621
642
|
members
|