@indigoai-us/hq-cli 5.345.32 → 5.345.34
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 +15 -0
- package/dist/command-catalog.generated.d.ts +92 -3
- package/dist/command-catalog.generated.js +111 -3
- package/dist/commands/cloud-provision.d.ts +2 -0
- package/dist/commands/db-dump.d.ts +8 -0
- package/dist/commands/db-dump.js +50 -0
- package/dist/commands/db-provision.d.ts +3 -10
- package/dist/commands/db-provision.js +26 -47
- package/dist/commands/db-remote-deps.d.ts +18 -0
- package/dist/commands/db-remote-deps.js +59 -0
- package/dist/commands/db-sql.d.ts +2 -1
- package/dist/commands/db-sql.js +30 -5
- package/dist/commands/db-usage.d.ts +7 -0
- package/dist/commands/db-usage.js +28 -0
- package/dist/commands/db.js +4 -0
- package/dist/commands/files.js +33 -5
- package/dist/commands/lanes-message.js +40 -8
- package/dist/commands/lanes-questions.d.ts +6 -1
- package/dist/commands/lanes-questions.js +19 -7
- package/dist/commands/lanes-resume.js +17 -6
- package/dist/commands/lanes-shared.d.ts +6 -1
- package/dist/commands/lanes.js +92 -15
- package/dist/commands/mesh.js +150 -26
- package/dist/commands/reindex.js +11 -8
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +68 -604
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/daemon/remote-control.js +62 -3
- package/dist/lib/db/control-plane.d.ts +32 -0
- package/dist/lib/db/control-plane.js +50 -0
- package/dist/lib/db/redact.d.ts +4 -0
- package/dist/lib/db/redact.js +17 -0
- package/dist/lib/db/remote-engine.d.ts +67 -0
- package/dist/lib/db/remote-engine.js +165 -0
- package/dist/lib/db/usage-format.d.ts +4 -0
- package/dist/lib/db/usage-format.js +41 -0
- package/dist/lib/index-render/shared.d.ts +3 -1
- package/dist/lib/index-render/shared.js +5 -2
- package/dist/lib/lanes/depth-cap.d.ts +7 -0
- package/dist/lib/lanes/depth-cap.js +41 -0
- package/dist/lib/lanes/keep-alive.d.ts +12 -0
- package/dist/lib/lanes/keep-alive.js +25 -6
- package/dist/lib/lanes/reconcile.d.ts +1 -1
- package/dist/lib/lanes/reconcile.js +16 -5
- package/dist/lib/mesh/api.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/install.d.ts +7 -4
- package/dist/lib/mesh/live/daemon/install.js +144 -47
- package/dist/lib/mesh/project-board.d.ts +17 -0
- package/dist/lib/mesh/project-board.js +28 -0
- package/dist/lib/mesh/story-status.d.ts +16 -0
- package/dist/lib/mesh/story-status.js +32 -7
- package/dist/utils/self-update.d.ts +2 -0
- package/dist/utils/self-update.js +61 -23
- package/dist/utils/version-check.d.ts +1 -1
- package/dist/utils/version-check.js +2 -2
- package/dist/utils/version-gate.d.ts +21 -14
- package/dist/utils/version-gate.js +119 -28
- package/package.json +4 -1
package/dist/commands/db-sql.js
CHANGED
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
import { Option } from "commander";
|
|
8
8
|
import chalk from "chalk";
|
|
9
9
|
import fs from "node:fs";
|
|
10
|
+
import { safeErrorMessage } from "../lib/db/redact.js";
|
|
11
|
+
import { formatRemoteResults, openRemoteAppConnection, runRemoteSql, } from "../lib/db/remote-engine.js";
|
|
10
12
|
import { formatSqlResult, runLocalSql } from "../lib/db/sql.js";
|
|
13
|
+
import { isPlanGateError } from "../utils/plan-gate-error.js";
|
|
14
|
+
import { defaultRemoteDeps, describeRemoteError, remoteEngineDeps, } from "./db-remote-deps.js";
|
|
15
|
+
import { describeSessionGap } from "./db-dump.js";
|
|
11
16
|
function readSqlArg(sqlArgs, stdinFlag) {
|
|
12
17
|
if (stdinFlag || sqlArgs.length === 0) {
|
|
13
18
|
if (process.stdin.isTTY && sqlArgs.length === 0) {
|
|
@@ -17,10 +22,11 @@ function readSqlArg(sqlArgs, stdinFlag) {
|
|
|
17
22
|
}
|
|
18
23
|
return sqlArgs.join(" ");
|
|
19
24
|
}
|
|
20
|
-
export function registerDbSqlCommand(db) {
|
|
25
|
+
export function registerDbSqlCommand(db, remoteDepsFactory = defaultRemoteDeps) {
|
|
21
26
|
db.command("sql")
|
|
22
|
-
.description("Run SQL against the company vault DB (local; read-only by default
|
|
27
|
+
.description("Run SQL against the company vault DB (local) or an app's remote database (--app); read-only by default")
|
|
23
28
|
.requiredOption("--company <slug>", "Company slug (tenant scope; required)")
|
|
29
|
+
.option("--app <app>", "Run against this app's remote database schema")
|
|
24
30
|
.option("--write", "Allow write/DDL statements (discouraged for ad-hoc DDL)", false)
|
|
25
31
|
.addOption(new Option("--remote", "Remote SQL is not available in this CLI build yet; local queries work without --remote").hideHelp())
|
|
26
32
|
.option("--db-path <path>", "Dangerous: open a non-canonical DB path (denied unless --allow-cross-company-path)")
|
|
@@ -37,13 +43,17 @@ export function registerDbSqlCommand(db) {
|
|
|
37
43
|
process.exitCode = 1;
|
|
38
44
|
return;
|
|
39
45
|
}
|
|
40
|
-
if (opts.remote) {
|
|
41
|
-
console.error(chalk.red("Error: Remote SQL
|
|
46
|
+
if (opts.remote && !opts.app) {
|
|
47
|
+
console.error(chalk.red("Error: Remote SQL needs --app <app>. Local queries work without --remote."));
|
|
42
48
|
process.exitCode = 1;
|
|
43
49
|
return;
|
|
44
50
|
}
|
|
45
51
|
const sql = readSqlArg(sqlParts, !!opts.stdin).trim();
|
|
46
52
|
const format = opts.format === "table" ? "table" : "jsonl";
|
|
53
|
+
if (opts.app) {
|
|
54
|
+
await runRemote(remoteDepsFactory(), company, opts.app, sql, !!opts.write);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
47
57
|
const result = runLocalSql({
|
|
48
58
|
company,
|
|
49
59
|
sql,
|
|
@@ -56,9 +66,24 @@ export function registerDbSqlCommand(db) {
|
|
|
56
66
|
console.log(formatSqlResult(result, format));
|
|
57
67
|
}
|
|
58
68
|
catch (error) {
|
|
59
|
-
|
|
69
|
+
if (isPlanGateError(error))
|
|
70
|
+
throw error;
|
|
71
|
+
console.error(chalk.red("Error:"), describeSessionGap(error) ?? describeRemoteError(error) ?? safeErrorMessage(error));
|
|
60
72
|
process.exitCode = 1;
|
|
61
73
|
}
|
|
62
74
|
});
|
|
63
75
|
}
|
|
76
|
+
async function runRemote(deps, company, app, sql, write) {
|
|
77
|
+
const resolved = await deps.resolveCompany(company);
|
|
78
|
+
const conn = await openRemoteAppConnection(remoteEngineDeps(deps), {
|
|
79
|
+
companyUid: resolved.companyUid,
|
|
80
|
+
appId: app,
|
|
81
|
+
});
|
|
82
|
+
try {
|
|
83
|
+
console.log(formatRemoteResults(await runRemoteSql(conn, sql, write)));
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
await conn.close();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
64
89
|
//# sourceMappingURL=db-sql.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hq db usage — month-to-date remote database usage against both caps.
|
|
3
|
+
*/
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { type DbRemoteCommandDeps } from "./db-remote-deps.js";
|
|
6
|
+
export declare function registerDbUsageCommand(db: Command, depsFactory?: () => DbRemoteCommandDeps): void;
|
|
7
|
+
//# sourceMappingURL=db-usage.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hq db usage — month-to-date remote database usage against both caps.
|
|
3
|
+
*/
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { safeErrorMessage } from "../lib/db/redact.js";
|
|
6
|
+
import { formatUsage } from "../lib/db/usage-format.js";
|
|
7
|
+
import { isPlanGateError } from "../utils/plan-gate-error.js";
|
|
8
|
+
import { controlPlaneClient, defaultRemoteDeps, describeRemoteError, } from "./db-remote-deps.js";
|
|
9
|
+
export function registerDbUsageCommand(db, depsFactory = defaultRemoteDeps) {
|
|
10
|
+
db.command("usage")
|
|
11
|
+
.description("Show this month's remote database usage against the compute and storage caps")
|
|
12
|
+
.requiredOption("--company <slug>", "Company slug")
|
|
13
|
+
.action(async (opts) => {
|
|
14
|
+
try {
|
|
15
|
+
const deps = depsFactory();
|
|
16
|
+
const resolved = await deps.resolveCompany(String(opts.company ?? "").trim());
|
|
17
|
+
const usage = await controlPlaneClient(deps).usage(resolved.companyUid);
|
|
18
|
+
console.log(formatUsage(usage));
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (isPlanGateError(error))
|
|
22
|
+
throw error;
|
|
23
|
+
console.error(chalk.red("Error:"), describeRemoteError(error) ?? safeErrorMessage(error));
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=db-usage.js.map
|
package/dist/commands/db.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* hq db — vault database commands (local + remote tiers).
|
|
3
3
|
*/
|
|
4
|
+
import { registerDbDumpCommand } from "./db-dump.js";
|
|
4
5
|
import { registerDbMigrateCommand } from "./db-migrate.js";
|
|
5
6
|
import { registerDbProvisionCommand } from "./db-provision.js";
|
|
6
7
|
import { registerDbSqlCommand } from "./db-sql.js";
|
|
7
8
|
import { registerDbStatusCommand } from "./db-status.js";
|
|
9
|
+
import { registerDbUsageCommand } from "./db-usage.js";
|
|
8
10
|
/**
|
|
9
11
|
* Register the `hq db` command group and its subcommands.
|
|
10
12
|
*/
|
|
@@ -16,5 +18,7 @@ export function registerDbCommand(program) {
|
|
|
16
18
|
registerDbSqlCommand(db);
|
|
17
19
|
registerDbMigrateCommand(db);
|
|
18
20
|
registerDbProvisionCommand(db);
|
|
21
|
+
registerDbUsageCommand(db);
|
|
22
|
+
registerDbDumpCommand(db);
|
|
19
23
|
}
|
|
20
24
|
//# sourceMappingURL=db.js.map
|
package/dist/commands/files.js
CHANGED
|
@@ -64,7 +64,9 @@ function formatAclPatternConflict(prefix, serverMessage) {
|
|
|
64
64
|
const sharedPattern = `${privatePattern}*`;
|
|
65
65
|
const detail = serverMessage ? `${serverMessage}. ` : "";
|
|
66
66
|
return (`${detail}A private folder '${privatePattern}' and shared folder glob ` +
|
|
67
|
-
`'${sharedPattern}' cannot both exist.
|
|
67
|
+
`'${sharedPattern}' cannot both exist. If the shared glob has no grants, run ` +
|
|
68
|
+
`hq files unshare '${sharedPattern}'. If it has grants, remove them first with ` +
|
|
69
|
+
`hq files unshare '${sharedPattern}' --with <principal>.`);
|
|
68
70
|
}
|
|
69
71
|
/**
|
|
70
72
|
* POST /files/{companyUid}/share-session — mint a browser-launch share
|
|
@@ -265,8 +267,8 @@ export function registerFilesCommand(program) {
|
|
|
265
267
|
});
|
|
266
268
|
files
|
|
267
269
|
.command("unshare <prefix>")
|
|
268
|
-
.description("Remove a file access grant
|
|
269
|
-
.
|
|
270
|
+
.description("Remove a named file access grant, or remove an empty ACL row without --with.")
|
|
271
|
+
.option("--with <principal>", "Person uid, agent uid, email address, group id, or '@all' to remove the grant")
|
|
270
272
|
.action(async (prefix, opts) => {
|
|
271
273
|
try {
|
|
272
274
|
// Unshare MUTATES access with no preview. Anchored content paths remain
|
|
@@ -279,13 +281,13 @@ export function registerFilesCommand(program) {
|
|
|
279
281
|
? { companySlug: files.opts().company }
|
|
280
282
|
: {}),
|
|
281
283
|
});
|
|
282
|
-
const principal = opts.with;
|
|
284
|
+
const principal = opts.with ?? "";
|
|
283
285
|
const isAll = principal === "@all";
|
|
284
286
|
const isEmail = !isAll && EMAIL_PATTERN.test(principal);
|
|
285
287
|
const isGroup = !isAll && GROUP_ID_PATTERN.test(principal);
|
|
286
288
|
const isAgent = !isAll && !isEmail && !isGroup && AGENT_UID_PATTERN.test(principal);
|
|
287
289
|
const isPerson = !isAll && !isEmail && !isGroup && !isAgent && PERSON_UID_PATTERN.test(principal);
|
|
288
|
-
if (!isAll && !isEmail && !isGroup && !isPerson && !isAgent) {
|
|
290
|
+
if (opts.with && !isAll && !isEmail && !isGroup && !isPerson && !isAgent) {
|
|
289
291
|
console.error(chalk.red(`Invalid principal '${principal}': must be '@all', an email address, a person uid matching prs_<alphanumeric>, an agent uid matching agt_<alphanumeric>, or a group id matching grp_<alphanumeric>`));
|
|
290
292
|
process.exit(1);
|
|
291
293
|
}
|
|
@@ -313,6 +315,32 @@ export function registerFilesCommand(program) {
|
|
|
313
315
|
explicit: explicitCompanySlug,
|
|
314
316
|
anchorSlug: companyRootSlug,
|
|
315
317
|
});
|
|
318
|
+
if (!opts.with) {
|
|
319
|
+
const res = await vaultApiFetch({
|
|
320
|
+
token,
|
|
321
|
+
path: `/files/${encodeURIComponent(companyUid)}/acl?prefix=${encodeURIComponent(canonicalPrefix)}`,
|
|
322
|
+
method: "DELETE",
|
|
323
|
+
});
|
|
324
|
+
const body = await res.json().catch(() => ({}));
|
|
325
|
+
if (res.status === 404 && body.code === "ACL_NOT_FOUND") {
|
|
326
|
+
console.log(chalk.dim(`Nothing to remove — '${canonicalPrefix}' has no ACL row.`));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (!res.ok) {
|
|
330
|
+
if (res.status === 403) {
|
|
331
|
+
console.error(chalk.red("Not authorized to remove this ACL row"));
|
|
332
|
+
await offerAccessLadder(canonicalPrefix, {
|
|
333
|
+
company: files.opts().company,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
console.error(chalk.red(body.error ?? "Unable to remove ACL row"));
|
|
338
|
+
}
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
console.log(chalk.green(`Removed empty ACL pattern '${canonicalPrefix}'. It now follows its parent folder's access, so anyone who can see that parent can see this folder.`));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
316
344
|
const res = await vaultApiFetch({
|
|
317
345
|
token,
|
|
318
346
|
path: `/files/${encodeURIComponent(companyUid)}/acl/revoke`,
|
|
@@ -11,7 +11,8 @@ import { isPidAlive, runWatcherLoop, WATCHER_OUT_MAX_BYTES, watcherOutLogPath, w
|
|
|
11
11
|
import { laneStories, laneStory, storyWarning } from "../lib/lanes/stories.js";
|
|
12
12
|
import { artifactIdentityKeys, rollbackStorySwitchAttribution, storySwitchNotice } from "../lib/lanes/story-switch.js";
|
|
13
13
|
import { laneRunDir, probePidRecord, readPidRecord, readProcStarttime, spawnLaneWorker } from "../lib/lanes/spawn.js";
|
|
14
|
-
import { keepAliveExclusion, keepAliveSupersededBy, LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_FLAG, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds } from "../lib/lanes/keep-alive.js";
|
|
14
|
+
import { findRetiredLaneEntry, keepAliveExclusion, keepAliveSupersededBy, LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_FLAG, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds } from "../lib/lanes/keep-alive.js";
|
|
15
|
+
import { isTerminalLaneState } from "../lib/lanes/types.js";
|
|
15
16
|
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
16
17
|
import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
|
|
17
18
|
import { accountMoveSummary, accountRequestKeepsCurrentLane, assertCurrentResumeAccountAvailable, callerMatchesSessionSenior, recordedResumeEffort, resetKeepAliveProgress, resolveResumeEffort } from "./lanes-shared.js";
|
|
@@ -136,6 +137,14 @@ export function createLaneMessageHandlers(deps) {
|
|
|
136
137
|
...(snapshot.hire_grant === undefined ? {} : { hire_grant: snapshot.hire_grant }),
|
|
137
138
|
};
|
|
138
139
|
}
|
|
140
|
+
function activeRetiredRegistryWarning(hqRoot, lane) {
|
|
141
|
+
if (lane.keep_alive !== true || isTerminalLaneState(lane.state))
|
|
142
|
+
return undefined;
|
|
143
|
+
const entry = findRetiredLaneEntry(hqRoot, lane.lane_id);
|
|
144
|
+
if (entry === undefined)
|
|
145
|
+
return undefined;
|
|
146
|
+
return `The active keep-alive lane record takes precedence over ${entry.path} retired[${entry.index}] (lane ${entry.lane}, retired_at ${entry.retired_at ?? "not recorded"}); remove the stale registry row from workspace/lane-briefs/juniors.json.`;
|
|
147
|
+
}
|
|
139
148
|
async function messageLaneExclusion(ctx, lane, grantContext) {
|
|
140
149
|
if (lane.state === "killed" || lane.state === "cancelled") {
|
|
141
150
|
return { lane, exclusion: lane.state };
|
|
@@ -172,6 +181,14 @@ export function createLaneMessageHandlers(deps) {
|
|
|
172
181
|
lanes = [...merged.values()];
|
|
173
182
|
}
|
|
174
183
|
let exclusion = keepAliveExclusion(currentLane, lanes, retiredLaneIds, lineageAware, companyForLane);
|
|
184
|
+
let retiredRegistryWarning = exclusion === "retired"
|
|
185
|
+
? activeRetiredRegistryWarning(ctx.hqRoot(), currentLane)
|
|
186
|
+
: undefined;
|
|
187
|
+
if (retiredRegistryWarning !== undefined) {
|
|
188
|
+
exclusion = keepAliveSupersededBy(currentLane, lanes, lineageAware, companyForLane) === undefined
|
|
189
|
+
? undefined
|
|
190
|
+
: "superseded";
|
|
191
|
+
}
|
|
175
192
|
if (exclusion === "superseded" && currentLane.state !== "done" && currentLane.state !== "interrupted") {
|
|
176
193
|
const runningSupersededMessageDeliveryEnabled = await readLanesRegistryFlag(LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_FLAG, { company: companyForLane(currentLane), env: ctx.env });
|
|
177
194
|
if (runningSupersededMessageDeliveryEnabled) {
|
|
@@ -203,6 +220,14 @@ export function createLaneMessageHandlers(deps) {
|
|
|
203
220
|
lanes = latestLanes;
|
|
204
221
|
retiredLaneIds = loadRetiredLaneIds(ctx.hqRoot());
|
|
205
222
|
exclusion = keepAliveExclusion(currentLane, lanes, retiredLaneIds, lineageAware, companyForLane);
|
|
223
|
+
retiredRegistryWarning = exclusion === "retired"
|
|
224
|
+
? activeRetiredRegistryWarning(ctx.hqRoot(), currentLane)
|
|
225
|
+
: undefined;
|
|
226
|
+
if (retiredRegistryWarning !== undefined) {
|
|
227
|
+
exclusion = keepAliveSupersededBy(currentLane, lanes, lineageAware, companyForLane) === undefined
|
|
228
|
+
? undefined
|
|
229
|
+
: "superseded";
|
|
230
|
+
}
|
|
206
231
|
if (exclusion === "superseded" && currentLane.state !== "done" && currentLane.state !== "interrupted") {
|
|
207
232
|
const callerLaneId = dispatchedContextBound ? dispatchedCallerId : undefined;
|
|
208
233
|
const callerSessionId = dispatchedContextBound
|
|
@@ -222,6 +247,7 @@ export function createLaneMessageHandlers(deps) {
|
|
|
222
247
|
? { supersededMessageSessionId: currentLane.senior.id }
|
|
223
248
|
: {}),
|
|
224
249
|
supersededMessageWarning: `A newer lead supersedes this ${currentLane.state} lane; message delivery is allowed while the lane remains active.`,
|
|
250
|
+
...(retiredRegistryWarning === undefined ? {} : { retiredRegistryWarning }),
|
|
225
251
|
};
|
|
226
252
|
}
|
|
227
253
|
}
|
|
@@ -234,6 +260,7 @@ export function createLaneMessageHandlers(deps) {
|
|
|
234
260
|
return {
|
|
235
261
|
lane: currentLane,
|
|
236
262
|
exclusion: undefined,
|
|
263
|
+
...(retiredRegistryWarning === undefined ? {} : { retiredRegistryWarning }),
|
|
237
264
|
...(!dispatchedContextAuthorized && ctx.env.HQ_LANE_ID?.trim()
|
|
238
265
|
? { supersededMessageCallerId: ctx.env.HQ_LANE_ID.trim() }
|
|
239
266
|
: {}),
|
|
@@ -245,7 +272,7 @@ export function createLaneMessageHandlers(deps) {
|
|
|
245
272
|
: {}),
|
|
246
273
|
};
|
|
247
274
|
}
|
|
248
|
-
return { lane: currentLane, exclusion };
|
|
275
|
+
return { lane: currentLane, exclusion, ...(retiredRegistryWarning === undefined ? {} : { retiredRegistryWarning }) };
|
|
249
276
|
}
|
|
250
277
|
function recordMessageUndeliverable(ctx, lane, reason, messageCount) {
|
|
251
278
|
writeWatcherDecision(watcherOutLogPath(ctx.hqRoot(), lane.lane_id), {
|
|
@@ -450,19 +477,23 @@ export function createLaneMessageHandlers(deps) {
|
|
|
450
477
|
if (opts.account !== undefined && !keepCurrentAccount && !providerSessionPortable(lane.provider)) {
|
|
451
478
|
throw new LanesError("provider_session_not_portable", sessionNotPortableMessage(lane.provider), { lane_id: laneId, provider: lane.provider });
|
|
452
479
|
}
|
|
453
|
-
const
|
|
454
|
-
? await deps.
|
|
455
|
-
:
|
|
456
|
-
if (
|
|
457
|
-
const
|
|
480
|
+
const pendingQuestions = lane.state === "awaiting_input"
|
|
481
|
+
? await deps.pendingQuestionsForLane(ctx, lane)
|
|
482
|
+
: [];
|
|
483
|
+
if (pendingQuestions.length > 0) {
|
|
484
|
+
const companySlug = lane.company?.slug ?? lane.mesh_company ?? "<company-slug>";
|
|
485
|
+
const instructions = pendingQuestions.map(({ question_id, audience }) => `question ${question_id} awaits ${audience.kind}:${audience.id}; answer it with hq lanes questions answer ${question_id} --company ${companySlug} --freeform <answer>`);
|
|
486
|
+
const message = `Lane ${laneId} is awaiting input. ${instructions.join("; ")}`;
|
|
458
487
|
throw new LanesError("lane_not_accepting_messages", message, {
|
|
459
488
|
lane_id: laneId,
|
|
460
489
|
state: lane.state,
|
|
490
|
+
pending_questions: pendingQuestions,
|
|
491
|
+
answer_commands: pendingQuestions.map(({ question_id }) => `hq lanes questions answer ${question_id} --company ${companySlug} --freeform <answer>`),
|
|
461
492
|
provider: lane.provider,
|
|
462
493
|
message,
|
|
463
494
|
});
|
|
464
495
|
}
|
|
465
|
-
const awaitingInputFallback = lane.state === "awaiting_input"
|
|
496
|
+
const awaitingInputFallback = lane.state === "awaiting_input";
|
|
466
497
|
// One decision point for how a follow-up reaches this lane (delivery.ts).
|
|
467
498
|
// `none` refuses before anything is written: no inbox append, no spawn.
|
|
468
499
|
const mechanism = laneDeliveryMechanism(lane);
|
|
@@ -915,6 +946,7 @@ export function createLaneMessageHandlers(deps) {
|
|
|
915
946
|
: "worker_spawned";
|
|
916
947
|
const warning = [
|
|
917
948
|
eligibility.supersededMessageWarning,
|
|
949
|
+
eligibility.retiredRegistryWarning,
|
|
918
950
|
mechanism === "dropbox" && drainHook !== null && !drainHook.registered
|
|
919
951
|
? `The drain hook is not registered in ${drainHook.path}; the running worker will not receive this message. It stays in the inbox and is delivered on the lane's next start.`
|
|
920
952
|
: mechanism === "dropbox" && next.stalled === true
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import { type QuestionBody, type LaneRecord, type QuestionRecord } from "../lib/lanes/index.js";
|
|
1
|
+
import { type QuestionBody, type LaneRecord, type QuestionRecord, type SeniorRef } from "../lib/lanes/index.js";
|
|
2
2
|
import type { Ctx, FallbackQuestionCancellationResult, LaneQuestionsHandlerDeps } from "./lanes-shared.js";
|
|
3
3
|
export declare function resolveAnswerChoice(choice: string, options: string[], freeform: boolean): string;
|
|
4
4
|
export declare function createLaneQuestionsHandlers(deps: LaneQuestionsHandlerDeps): {
|
|
5
5
|
hasPendingQuestionForLane: (ctx: Ctx, lane: LaneRecord) => Promise<boolean>;
|
|
6
|
+
pendingQuestionsForLane: (ctx: Ctx, lane: LaneRecord) => Promise<Array<{
|
|
7
|
+
question_id: string;
|
|
8
|
+
asked_at: string;
|
|
9
|
+
audience: SeniorRef;
|
|
10
|
+
}>>;
|
|
6
11
|
cancelFallbackQuestionsForLane: (ctx: Ctx, lane: LaneRecord, trigger: "message" | "resume", at: string) => Promise<FallbackQuestionCancellationResult>;
|
|
7
12
|
runQuestionsList: (ctx: Ctx, opts: {
|
|
8
13
|
allHosts?: boolean;
|
|
@@ -488,21 +488,32 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
488
488
|
}
|
|
489
489
|
return { cancelled_question_ids: cancelledQuestionIds, failures };
|
|
490
490
|
}
|
|
491
|
-
async function
|
|
491
|
+
async function pendingQuestionsForLane(ctx, lane) {
|
|
492
492
|
const hqRoot = ctx.hqRoot();
|
|
493
|
-
const local = loadAllQuestions(hqRoot).
|
|
493
|
+
const local = loadAllQuestions(hqRoot).filter((question) => question.lane_id === lane.lane_id &&
|
|
494
494
|
question.status === "pending" &&
|
|
495
495
|
questionAudienceMatchesSenior(resolveAudience(hqRoot, question, ctx), lane, ctx));
|
|
496
|
-
|
|
497
|
-
|
|
496
|
+
const localRows = local.map((question) => ({
|
|
497
|
+
question_id: question.question_id,
|
|
498
|
+
asked_at: question.asked_at,
|
|
499
|
+
audience: resolveAudience(hqRoot, question, ctx),
|
|
500
|
+
}));
|
|
498
501
|
if (lane.host.kind !== "outpost" || laneIsHostedHere(ctx, lane))
|
|
499
|
-
return
|
|
502
|
+
return localRows;
|
|
500
503
|
// Reuse the owner lookup used by questions list/answer. An unreachable
|
|
501
504
|
// owner leaves no answerable question, so the message/resume path can recover.
|
|
502
505
|
const lookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot);
|
|
503
|
-
|
|
506
|
+
const remoteRows = lookup.questions.filter(({ question, lane: ownerLane, audience }) => ownerLane.lane_id === lane.lane_id &&
|
|
504
507
|
question.status === "pending" &&
|
|
505
|
-
questionAudienceMatchesSenior(audience, ownerLane, ctx))
|
|
508
|
+
questionAudienceMatchesSenior(audience, ownerLane, ctx)).map(({ question, audience }) => ({
|
|
509
|
+
question_id: question.question_id,
|
|
510
|
+
asked_at: question.asked_at,
|
|
511
|
+
audience,
|
|
512
|
+
}));
|
|
513
|
+
return [...localRows, ...remoteRows];
|
|
514
|
+
}
|
|
515
|
+
async function hasPendingQuestionForLane(ctx, lane) {
|
|
516
|
+
return (await pendingQuestionsForLane(ctx, lane)).length > 0;
|
|
506
517
|
}
|
|
507
518
|
function syncPrimaryQuestionDelivery(hqRoot, owner) {
|
|
508
519
|
const deliveredAt = owner.question.delivered_at;
|
|
@@ -991,6 +1002,7 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
991
1002
|
}
|
|
992
1003
|
return {
|
|
993
1004
|
hasPendingQuestionForLane,
|
|
1005
|
+
pendingQuestionsForLane,
|
|
994
1006
|
cancelFallbackQuestionsForLane,
|
|
995
1007
|
runQuestionsList,
|
|
996
1008
|
runQuestionsShow,
|
|
@@ -181,9 +181,20 @@ export function createLaneResumeHandlers(deps) {
|
|
|
181
181
|
opts.compact === false)) {
|
|
182
182
|
throw new LanesError("worker_profile_recovery_options_invalid", "Queued worker-profile recovery accepts only --worker; rerun hq lanes resume after reconcile admits the lane", { lane_id: laneId });
|
|
183
183
|
}
|
|
184
|
-
const
|
|
185
|
-
? await deps.
|
|
186
|
-
:
|
|
184
|
+
const pendingQuestions = lane.state === "awaiting_input"
|
|
185
|
+
? await deps.pendingQuestionsForLane(ctx, lane)
|
|
186
|
+
: [];
|
|
187
|
+
if (pendingQuestions.length > 0) {
|
|
188
|
+
const companySlug = lane.company?.slug ?? lane.mesh_company ?? "<company-slug>";
|
|
189
|
+
const instructions = pendingQuestions.map(({ question_id, audience }) => `question ${question_id} awaits ${audience.kind}:${audience.id}; answer it with hq lanes questions answer ${question_id} --company ${companySlug} --freeform <answer>`);
|
|
190
|
+
const message = `Lane ${laneId} is awaiting input. ${instructions.join("; ")}`;
|
|
191
|
+
throw new LanesError("lane_pending_questions", message, {
|
|
192
|
+
lane_id: laneId,
|
|
193
|
+
state: lane.state,
|
|
194
|
+
pending_questions: pendingQuestions,
|
|
195
|
+
answer_commands: pendingQuestions.map(({ question_id }) => `hq lanes questions answer ${question_id} --company ${companySlug} --freeform <answer>`),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
187
198
|
const projectId = opts.project?.trim() || lane.project_id;
|
|
188
199
|
const storyId = opts.story?.trim() || lane.story_id;
|
|
189
200
|
if (opts.story !== undefined && !opts.story.trim()) {
|
|
@@ -206,7 +217,7 @@ export function createLaneResumeHandlers(deps) {
|
|
|
206
217
|
throw new LanesError("profile_provider_changed", `Worker profile now selects ${effectiveProvider}; this lane runs ${lane.provider}; create a new lane`, { lane_id: lane.lane_id, lane_provider: lane.provider, profile_provider: effectiveProvider });
|
|
207
218
|
}
|
|
208
219
|
}
|
|
209
|
-
const refusal = queuedProfileRecovery ||
|
|
220
|
+
const refusal = queuedProfileRecovery || lane.state === "awaiting_input"
|
|
210
221
|
? null
|
|
211
222
|
: resumeCommandRefusal(lane, { allowFailed: opts.fromFailed === true });
|
|
212
223
|
if (refusal !== null) {
|
|
@@ -510,7 +521,7 @@ export function createLaneResumeHandlers(deps) {
|
|
|
510
521
|
trigger: {
|
|
511
522
|
kind: "resume",
|
|
512
523
|
from: resumedFrom,
|
|
513
|
-
...(resumedFrom === "awaiting_input" &&
|
|
524
|
+
...(resumedFrom === "awaiting_input" && pendingQuestions.length === 0 ? { allowAwaitingInput: true } : {}),
|
|
514
525
|
...(opts.fromFailed === true ? { fromFailed: true } : {}),
|
|
515
526
|
...(failedResumeExpectedSenior !== undefined
|
|
516
527
|
? { expectedSenior: failedResumeExpectedSenior }
|
|
@@ -527,7 +538,7 @@ export function createLaneResumeHandlers(deps) {
|
|
|
527
538
|
}, hireGrantContext);
|
|
528
539
|
}, ctx.sleep);
|
|
529
540
|
}
|
|
530
|
-
const questionCancellation = resumedFrom === "awaiting_input" &&
|
|
541
|
+
const questionCancellation = resumedFrom === "awaiting_input" && pendingQuestions.length === 0
|
|
531
542
|
? await deps.cancelFallbackQuestionsForLane(ctx, lane, "resume", at)
|
|
532
543
|
: null;
|
|
533
544
|
if (deliveryText !== undefined && messageId !== undefined) {
|
|
@@ -197,6 +197,11 @@ export interface LaneMessageHandlerDeps {
|
|
|
197
197
|
resolveCommandLane(ctx: Ctx, laneId: string): Promise<CommandLaneResolution>;
|
|
198
198
|
resolveActiveQuestionCompany(ctx: Ctx, message?: string): Promise<LaneCompanyRef>;
|
|
199
199
|
hasPendingQuestionForLane(ctx: Ctx, lane: LaneRecord): Promise<boolean>;
|
|
200
|
+
pendingQuestionsForLane(ctx: Ctx, lane: LaneRecord): Promise<Array<{
|
|
201
|
+
question_id: string;
|
|
202
|
+
asked_at: string;
|
|
203
|
+
audience: SeniorRef;
|
|
204
|
+
}>>;
|
|
200
205
|
cancelFallbackQuestionsForLane(ctx: Ctx, lane: LaneRecord, trigger: "message" | "resume", at: string): Promise<FallbackQuestionCancellationResult>;
|
|
201
206
|
resolveResumeProfile(ctx: Ctx, lane: LaneRecord, workerId: string, allowProviderChange?: boolean): ResolvedResumeProfile;
|
|
202
207
|
reuseProfileRefreshEnabled(ctx: Ctx, lane: LaneRecord): Promise<boolean>;
|
|
@@ -211,7 +216,7 @@ export interface LaneMessageHandlerDeps {
|
|
|
211
216
|
validateResumeProfileModel(ctx: Ctx, lane: LaneRecord, profile: WorkerProfile): void;
|
|
212
217
|
withResumeHireAdmission<T>(ctx: Ctx, lane: LaneRecord, fn: () => T, grantContext?: OutpostHireGrantContext): Promise<T>;
|
|
213
218
|
}
|
|
214
|
-
export interface LaneResumeHandlerDeps extends Pick<LaneMessageHandlerDeps, "onLaneWorkerStarted" | "archiveResumeMessage" | "assertLaneRunsHereForAdmission" | "assertLaneRunsHereForOperatorMutation" | "assessLaneAdmission" | "casUpdateLane" | "laneIsHostedHere" | "laneWithRecoveredProviderSession" | "mintMessageId" | "prepareAccountMove" | "persistClaudeAccountSelection" | "printJson" | "recordResumeSetup" | "resolveCommandLane" | "hasPendingQuestionForLane" | "cancelFallbackQuestionsForLane" | "resolveResumeProfile" | "reuseProfileRefreshEnabled" | "selectAutomaticClaudeResumeAccount" | "validateResumeProfileModel" | "withResumeHireAdmission"> {
|
|
219
|
+
export interface LaneResumeHandlerDeps extends Pick<LaneMessageHandlerDeps, "onLaneWorkerStarted" | "archiveResumeMessage" | "assertLaneRunsHereForAdmission" | "assertLaneRunsHereForOperatorMutation" | "assessLaneAdmission" | "casUpdateLane" | "laneIsHostedHere" | "laneWithRecoveredProviderSession" | "mintMessageId" | "prepareAccountMove" | "persistClaudeAccountSelection" | "printJson" | "recordResumeSetup" | "resolveCommandLane" | "hasPendingQuestionForLane" | "pendingQuestionsForLane" | "cancelFallbackQuestionsForLane" | "resolveResumeProfile" | "reuseProfileRefreshEnabled" | "selectAutomaticClaudeResumeAccount" | "validateResumeProfileModel" | "withResumeHireAdmission"> {
|
|
215
220
|
dispatchOutpostLaneCommand(ctx: Ctx, lane: LaneRecord, action: "resume", options: LaneResumeOptions, behavior?: OutpostLaneDispatchBehavior): Promise<void>;
|
|
216
221
|
authorizeFailedResume(ctx: Ctx, lane: LaneRecord, encodedContext?: string): string;
|
|
217
222
|
authorizeResumeProfileRepin(ctx: Ctx, lane: LaneRecord, profile: ResolvedResumeProfile | undefined, encodedContext: string | undefined): ResumeProfileRepinAuthorization | undefined;
|