@indigoai-us/hq-cli 5.345.135 → 5.345.137
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 +44 -0
- package/assets/scaffold/core/scripts/worktree-gc.sh +98 -8
- package/dist/command-catalog.generated.d.ts +17 -6
- package/dist/command-catalog.generated.js +23 -7
- package/dist/commands/core-checkpoint-native.d.ts +10 -0
- package/dist/commands/core-checkpoint-native.js +60 -15
- package/dist/commands/daemon.js +3 -1
- package/dist/commands/integrations-core.d.ts +22 -0
- package/dist/commands/integrations-core.js +25 -0
- package/dist/commands/integrations-manage.d.ts +24 -1
- package/dist/commands/integrations-manage.js +92 -17
- package/dist/commands/integrations.d.ts +11 -0
- package/dist/commands/integrations.js +100 -19
- package/dist/commands/lanes-loop.d.ts +9 -0
- package/dist/commands/lanes-loop.js +9 -1
- package/dist/commands/lanes.js +69 -54
- package/dist/commands/meetings.d.ts +2 -0
- package/dist/commands/meetings.js +57 -14
- package/dist/commands/mesh.d.ts +19 -1
- package/dist/commands/mesh.js +158 -70
- package/dist/commands/reindex.js +80 -5
- package/dist/commands/storage-offload.d.ts +93 -28
- package/dist/commands/storage-offload.js +290 -34
- package/dist/commands/storage.js +60 -7
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2550 -1748
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/index.js +4 -0
- package/dist/lib/capacity/route.js +24 -0
- package/dist/lib/core-utils/session-company.d.ts +11 -0
- package/dist/lib/core-utils/session-company.js +34 -0
- package/dist/lib/core-utils/worktree-resume.js +44 -4
- package/dist/lib/core-utils/worktree-session-owner.d.ts +27 -0
- package/dist/lib/core-utils/worktree-session-owner.js +157 -0
- package/dist/lib/daemon/catalog.d.ts +2 -0
- package/dist/lib/daemon/catalog.js +3 -3
- package/dist/lib/daemon/hq-child-bootstrap-stage.d.ts +5 -0
- package/dist/lib/daemon/hq-child-bootstrap-stage.js +3 -11
- package/dist/lib/daemon/server.js +10 -1
- package/dist/lib/daemon/supervisor.js +6 -0
- package/dist/lib/daemon/sync-runner-bundle.d.ts +5 -0
- package/dist/lib/daemon/sync-runner-bundle.js +4 -12
- package/dist/lib/doctor/checks/claude-wiring.js +2 -1
- package/dist/lib/doctor/fix/apply.d.ts +20 -0
- package/dist/lib/doctor/fix/apply.js +63 -3
- package/dist/lib/doctor/fix/remediation.d.ts +1 -1
- package/dist/lib/doctor/fix/remediation.js +16 -0
- package/dist/lib/doctor/shared.d.ts +11 -1
- package/dist/lib/doctor/shared.js +16 -2
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/lanes/monitor.js +14 -2
- package/dist/lib/mesh/live/daemon/install.d.ts +12 -0
- package/dist/lib/mesh/live/daemon/install.js +22 -0
- package/dist/lib/mesh/live/flush.js +106 -3
- package/dist/lib/mesh/live/session-events-client.d.ts +9 -0
- package/dist/lib/mesh/live/session-events-client.js +15 -0
- package/dist/lib/work-context/state.d.ts +10 -0
- package/dist/lib/work-context/state.js +5 -0
- package/dist/utils/fsync-directory.d.ts +17 -0
- package/dist/utils/fsync-directory.js +27 -0
- package/dist/utils/windows-hide-default.d.ts +42 -0
- package/dist/utils/windows-hide-default.js +98 -0
- package/package.json +2 -2
|
@@ -17,7 +17,7 @@ import chalk from "chalk";
|
|
|
17
17
|
import { ensureCognitoIdToken } from "../utils/cognito-session.js";
|
|
18
18
|
import { getCompanyUid } from "../utils/vault-api.js";
|
|
19
19
|
import { redactSensitiveUrl, redactUrlsInText } from "../utils/redact-url.js";
|
|
20
|
-
import { IntegrationsCliError, bareProvider, printHealthNotice, fetchConnections, fetchAdminSurface, printJson, revokedConnectionDetails, resolveConnection, selectConnection, } from "./integrations-core.js";
|
|
20
|
+
import { IntegrationsCliError, bareProvider, printHealthNotice, fetchConnections, fetchAdminSurface, fetchAuditPage, AUDIT_PAGE_MAX, printJson, revokedConnectionDetails, resolveConnection, selectConnection, } from "./integrations-core.js";
|
|
21
21
|
import { fetchPendingApprovals, getConnectionAccess, mutateConnectionAccess, purgeConnection, setReadSafe, uninstallIntegration, updateGovernance, } from "./integrations-api.js";
|
|
22
22
|
const WRITE_POLICIES = ["auto-allow", "confirm", "deny"];
|
|
23
23
|
const PERMISSIONS = ["read", "write", "admin"];
|
|
@@ -520,30 +520,36 @@ export function registerManageCommands(integrations) {
|
|
|
520
520
|
.option("--company <slug>", "Company slug, e.g. indigo")
|
|
521
521
|
.option("--provider <slug>", "Only this app")
|
|
522
522
|
.option("--limit <n>", "How many events to show", "20")
|
|
523
|
+
.option("--since <iso>", "Only events at or after this ISO timestamp, e.g. 2026-10-01T00:00:00Z")
|
|
523
524
|
.option("--json", "Machine-readable output")
|
|
524
525
|
.action(async (opts) => {
|
|
525
|
-
const token = await ensureCognitoIdToken();
|
|
526
|
-
const companyUid = await getCompanyUid(token, opts.company);
|
|
527
|
-
const surface = await fetchAdminSurface(token, companyUid);
|
|
528
526
|
const limit = Number(opts.limit);
|
|
529
527
|
if (!Number.isFinite(limit) || limit < 1) {
|
|
530
528
|
throw new IntegrationsCliError("--limit must be a positive number.", { expected: true });
|
|
531
529
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
530
|
+
let sinceMs;
|
|
531
|
+
if (opts.since !== undefined) {
|
|
532
|
+
sinceMs = Date.parse(opts.since);
|
|
533
|
+
if (Number.isNaN(sinceMs)) {
|
|
534
|
+
throw new IntegrationsCliError("--since must be an ISO timestamp, e.g. 2026-10-01T00:00:00Z.", { expected: true });
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
const token = await ensureCognitoIdToken();
|
|
538
|
+
const companyUid = await getCompanyUid(token, opts.company);
|
|
539
|
+
const result = await collectAuditRows(token, companyUid, {
|
|
540
|
+
limit: Math.floor(limit),
|
|
541
|
+
provider: opts.provider,
|
|
542
|
+
sinceMs,
|
|
543
|
+
});
|
|
544
|
+
const { rows } = result;
|
|
540
545
|
if (opts.json) {
|
|
541
546
|
printJson(rows);
|
|
547
|
+
if (result.pageCapHit)
|
|
548
|
+
console.error(auditPageCapNote());
|
|
542
549
|
return;
|
|
543
550
|
}
|
|
544
551
|
if (rows.length === 0) {
|
|
545
552
|
console.log("No recorded activity yet.");
|
|
546
|
-
return;
|
|
547
553
|
}
|
|
548
554
|
for (const row of rows) {
|
|
549
555
|
const who = row.memberOrAgentName ?? row.memberOrAgent;
|
|
@@ -551,10 +557,21 @@ export function registerManageCommands(integrations) {
|
|
|
551
557
|
if (row.reason)
|
|
552
558
|
console.log(chalk.dim(` ${redactUrlsInText(row.reason)}`));
|
|
553
559
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
560
|
+
if (!result.paged) {
|
|
561
|
+
// An older hq-pro ignores view=audit and returns its surface feed,
|
|
562
|
+
// capped at 50 events; say so rather than letting a truncated list
|
|
563
|
+
// read as "that is everything that ever happened".
|
|
564
|
+
if (result.feedSize > rows.length && rows.length > 0) {
|
|
565
|
+
console.log(chalk.dim(`\nShowing ${rows.length} of ${result.feedSize} recent events.`));
|
|
566
|
+
}
|
|
567
|
+
console.log(chalk.dim("This server returns only the latest 50 events."));
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (result.pageCapHit) {
|
|
571
|
+
console.log(chalk.dim(`\n${auditPageCapNote()}`));
|
|
572
|
+
}
|
|
573
|
+
else if (result.more && rows.length > 0) {
|
|
574
|
+
console.log(chalk.dim(`\nShowing ${rows.length} events. Raise --limit or use --since to see more.`));
|
|
558
575
|
}
|
|
559
576
|
});
|
|
560
577
|
integrations
|
|
@@ -757,4 +774,62 @@ export function removeGrant(current, toolName, principal) {
|
|
|
757
774
|
}
|
|
758
775
|
return removed ? next : null;
|
|
759
776
|
}
|
|
777
|
+
/** Upper bound on audit pages one `hq integrations audit` run reads. */
|
|
778
|
+
export const AUDIT_MAX_PAGES = 200;
|
|
779
|
+
function auditPageCapNote() {
|
|
780
|
+
return `Stopped after reading ${AUDIT_MAX_PAGES} pages of activity; narrow the search with --since or --provider.`;
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Page the audit feed (newest first) until `limit` rows match, the cursor runs
|
|
784
|
+
* out, a row is older than `sinceMs`, or AUDIT_MAX_PAGES pages were read.
|
|
785
|
+
* `provider` is matched client-side on the bare slug, so the qualified
|
|
786
|
+
* `factory:linear` form copied from JSON output matches bare stored rows.
|
|
787
|
+
*/
|
|
788
|
+
export async function collectAuditRows(token, companyUid, opts) {
|
|
789
|
+
const want = opts.provider ? bareProvider(opts.provider.trim()).toLowerCase() : undefined;
|
|
790
|
+
const matches = (row) => !want || bareProvider(row.provider ?? "").toLowerCase() === want;
|
|
791
|
+
const tooOld = (row) => {
|
|
792
|
+
if (opts.sinceMs === undefined)
|
|
793
|
+
return false;
|
|
794
|
+
const at = Date.parse(row.timestamp);
|
|
795
|
+
return !Number.isNaN(at) && at < opts.sinceMs;
|
|
796
|
+
};
|
|
797
|
+
const rows = [];
|
|
798
|
+
let cursor;
|
|
799
|
+
for (let page = 0; page < AUDIT_MAX_PAGES; page += 1) {
|
|
800
|
+
const remaining = opts.limit - rows.length;
|
|
801
|
+
// With a provider filter most rows may not match, so read full pages.
|
|
802
|
+
const pageLimit = want ? AUDIT_PAGE_MAX : Math.min(AUDIT_PAGE_MAX, remaining);
|
|
803
|
+
const res = await fetchAuditPage(token, companyUid, { limit: pageLimit, cursor });
|
|
804
|
+
if (!res.paged) {
|
|
805
|
+
if (page > 0)
|
|
806
|
+
return { rows, paged: true, feedSize: 0, more: false, pageCapHit: false };
|
|
807
|
+
const fallback = res.audit.filter((row) => !tooOld(row) && matches(row));
|
|
808
|
+
return {
|
|
809
|
+
rows: fallback.slice(0, opts.limit),
|
|
810
|
+
paged: false,
|
|
811
|
+
feedSize: res.audit.length,
|
|
812
|
+
more: fallback.length > opts.limit,
|
|
813
|
+
pageCapHit: false,
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
for (let i = 0; i < res.audit.length; i += 1) {
|
|
817
|
+
const row = res.audit[i];
|
|
818
|
+
if (tooOld(row))
|
|
819
|
+
return { rows, paged: true, feedSize: 0, more: false, pageCapHit: false };
|
|
820
|
+
if (!matches(row))
|
|
821
|
+
continue;
|
|
822
|
+
if (rows.length >= opts.limit) {
|
|
823
|
+
return { rows, paged: true, feedSize: 0, more: true, pageCapHit: false };
|
|
824
|
+
}
|
|
825
|
+
rows.push(row);
|
|
826
|
+
}
|
|
827
|
+
if (!res.nextCursor)
|
|
828
|
+
return { rows, paged: true, feedSize: 0, more: false, pageCapHit: false };
|
|
829
|
+
if (rows.length >= opts.limit)
|
|
830
|
+
return { rows, paged: true, feedSize: 0, more: true, pageCapHit: false };
|
|
831
|
+
cursor = res.nextCursor;
|
|
832
|
+
}
|
|
833
|
+
return { rows, paged: true, feedSize: 0, more: true, pageCapHit: true };
|
|
834
|
+
}
|
|
760
835
|
//# sourceMappingURL=integrations-manage.js.map
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* hq integrations show [app] One app in full.
|
|
22
22
|
* hq integrations tools --provider <p> What the connected app can do.
|
|
23
23
|
* hq integrations call <tool> --provider <p> --args '<json>'
|
|
24
|
+
* hq integrations call <tool> --provider <p> --args-file args.json
|
|
24
25
|
* hq integrations pending Calls awaiting approval.
|
|
25
26
|
* hq integrations pending --history Recently decided calls, including status.
|
|
26
27
|
* hq integrations approve|reject <queueId> Decide a queued call.
|
|
@@ -51,6 +52,16 @@
|
|
|
51
52
|
*/
|
|
52
53
|
import { Command } from "commander";
|
|
53
54
|
import { type PendingApproval } from "./integrations-api.js";
|
|
55
|
+
/**
|
|
56
|
+
* Resolve `hq integrations call` tool arguments from `--args <json>`,
|
|
57
|
+
* `--args -` (stdin), or `--args-file <path>` (`-` also reads stdin). A file or
|
|
58
|
+
* stdin avoids shell quoting: zsh breaks inline JSON that contains apostrophes
|
|
59
|
+
* or parentheses. The result must be a JSON object.
|
|
60
|
+
*/
|
|
61
|
+
export declare function resolveCallArgs(opts: {
|
|
62
|
+
args?: string;
|
|
63
|
+
argsFile?: string;
|
|
64
|
+
}, stdin?: NodeJS.ReadableStream): Promise<Record<string, unknown>>;
|
|
54
65
|
export { IntegrationsCliError, callGateway, fetchConnections, fetchUsableApps, queuedOutcome, selectConnection, toolPrefixForProvider, unwrapGatewayResult, } from "./integrations-core.js";
|
|
55
66
|
export type { AdminConnection, UsableApp, UsableAppsResult } from "./integrations-core.js";
|
|
56
67
|
/** Wait for a decided queue item without resending the approve request. */
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* hq integrations show [app] One app in full.
|
|
22
22
|
* hq integrations tools --provider <p> What the connected app can do.
|
|
23
23
|
* hq integrations call <tool> --provider <p> --args '<json>'
|
|
24
|
+
* hq integrations call <tool> --provider <p> --args-file args.json
|
|
24
25
|
* hq integrations pending Calls awaiting approval.
|
|
25
26
|
* hq integrations pending --history Recently decided calls, including status.
|
|
26
27
|
* hq integrations approve|reject <queueId> Decide a queued call.
|
|
@@ -50,6 +51,7 @@
|
|
|
50
51
|
* `integrations-manage.ts`.
|
|
51
52
|
*/
|
|
52
53
|
import { randomUUID } from "node:crypto";
|
|
54
|
+
import { readFile } from "node:fs/promises";
|
|
53
55
|
import chalk from "chalk";
|
|
54
56
|
import { ensureCognitoIdToken } from "../utils/cognito-session.js";
|
|
55
57
|
import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
|
|
@@ -60,6 +62,69 @@ import { registerImportCommands } from "./integrations-import.js";
|
|
|
60
62
|
import { registerManageCommands } from "./integrations-manage.js";
|
|
61
63
|
import { registerSlackCommands } from "./integrations-slack.js";
|
|
62
64
|
import { fetchPendingApprovals } from "./integrations-api.js";
|
|
65
|
+
const CALL_ARGS_EXAMPLE = `--args '{"assignee":"me"}' or --args-file args.json`;
|
|
66
|
+
function readStream(stream) {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
let data = "";
|
|
69
|
+
stream.setEncoding("utf8");
|
|
70
|
+
stream.on("data", (chunk) => {
|
|
71
|
+
data += chunk;
|
|
72
|
+
});
|
|
73
|
+
stream.on("end", () => resolve(data));
|
|
74
|
+
stream.on("error", reject);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Resolve `hq integrations call` tool arguments from `--args <json>`,
|
|
79
|
+
* `--args -` (stdin), or `--args-file <path>` (`-` also reads stdin). A file or
|
|
80
|
+
* stdin avoids shell quoting: zsh breaks inline JSON that contains apostrophes
|
|
81
|
+
* or parentheses. The result must be a JSON object.
|
|
82
|
+
*/
|
|
83
|
+
export async function resolveCallArgs(opts, stdin = process.stdin) {
|
|
84
|
+
if (opts.args !== undefined && opts.argsFile !== undefined) {
|
|
85
|
+
throw new IntegrationsCliError("Use either --args or --args-file, not both.", { expected: true });
|
|
86
|
+
}
|
|
87
|
+
let text;
|
|
88
|
+
let source;
|
|
89
|
+
const fromStdin = opts.args === "-" || opts.argsFile === "-";
|
|
90
|
+
if (fromStdin) {
|
|
91
|
+
if (stdin.isTTY) {
|
|
92
|
+
throw new IntegrationsCliError("Reading tool arguments from stdin needs piped input, e.g. `hq integrations call <tool> --args - < args.json`.", { expected: true });
|
|
93
|
+
}
|
|
94
|
+
text = await readStream(stdin);
|
|
95
|
+
source = "stdin";
|
|
96
|
+
}
|
|
97
|
+
else if (opts.argsFile !== undefined) {
|
|
98
|
+
source = `--args-file ${opts.argsFile}`;
|
|
99
|
+
try {
|
|
100
|
+
text = await readFile(opts.argsFile, "utf8");
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
104
|
+
throw new IntegrationsCliError(`Could not read --args-file ${opts.argsFile}: ${reason}`, { expected: true });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
text = opts.args ?? "{}";
|
|
109
|
+
source = "--args";
|
|
110
|
+
}
|
|
111
|
+
let raw;
|
|
112
|
+
try {
|
|
113
|
+
raw = JSON.parse(text);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
117
|
+
throw new IntegrationsCliError(source === "--args"
|
|
118
|
+
? `--args must be a JSON object, e.g. ${CALL_ARGS_EXAMPLE}`
|
|
119
|
+
: `Tool arguments from ${source} are not valid JSON (${reason}). They must be a JSON object, e.g. {"assignee":"me"}.`, { expected: true });
|
|
120
|
+
}
|
|
121
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
122
|
+
throw new IntegrationsCliError(source === "--args"
|
|
123
|
+
? `--args must be a JSON object, e.g. ${CALL_ARGS_EXAMPLE}`
|
|
124
|
+
: `Tool arguments from ${source} must be a JSON object, e.g. {"assignee":"me"}.`, { expected: true });
|
|
125
|
+
}
|
|
126
|
+
return raw;
|
|
127
|
+
}
|
|
63
128
|
/** Routing ids hq sets on a built-in tool call; never accepted from --args. */
|
|
64
129
|
const BUILT_IN_ROUTING_ARGS = ["companyUid", "connectionId", "idempotencyKey"];
|
|
65
130
|
/**
|
|
@@ -382,7 +447,7 @@ export function registerIntegrationsCommand(program) {
|
|
|
382
447
|
const selector = sharedProvider
|
|
383
448
|
? `--connection ${connection.id}`
|
|
384
449
|
: `--provider ${bareProvider(connection.provider)}`;
|
|
385
|
-
console.log(chalk.dim(`\n${tools.length} tools. Describe inputs: hq integrations tools ${selector} --describe <tool>\nCall one with: hq integrations call <tool> ${selector} --args '<json>'`));
|
|
450
|
+
console.log(chalk.dim(`\n${tools.length} tools. Describe inputs: hq integrations tools ${selector} --describe <tool>\nCall one with: hq integrations call <tool> ${selector} --args '<json>'\nIf the JSON has apostrophes or parentheses, put it in a file: --args-file args.json (or --args - to read stdin)`));
|
|
386
451
|
});
|
|
387
452
|
integrations
|
|
388
453
|
.command("call <tool>")
|
|
@@ -390,21 +455,12 @@ export function registerIntegrationsCommand(program) {
|
|
|
390
455
|
.option("--provider <slug>", "Connected app (e.g. linear)")
|
|
391
456
|
.option("--connection <id>", "Connection id (acct_…)")
|
|
392
457
|
.option("--company <slug>", "Company slug, e.g. indigo")
|
|
393
|
-
.option("--args <json>", "Tool arguments as JSON
|
|
458
|
+
.option("--args <json>", "Tool arguments as a JSON object (default {}); use - to read the JSON from stdin")
|
|
459
|
+
.option("--args-file <path>", "Read tool arguments from a JSON file (use - for stdin); avoids shell quoting of apostrophes and parentheses")
|
|
394
460
|
.option("--idempotency-key <key>", "Stable key so retries execute at most once (default: generated)")
|
|
395
461
|
.option("--json", "Machine-readable output")
|
|
396
462
|
.action(async (tool, opts) => {
|
|
397
|
-
|
|
398
|
-
try {
|
|
399
|
-
const raw = JSON.parse(opts.args);
|
|
400
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
401
|
-
throw new Error("not an object");
|
|
402
|
-
}
|
|
403
|
-
parsedArgs = raw;
|
|
404
|
-
}
|
|
405
|
-
catch {
|
|
406
|
-
throw new IntegrationsCliError(`--args must be a JSON object, e.g. --args '{"assignee":"me"}'`, { expected: true });
|
|
407
|
-
}
|
|
463
|
+
const parsedArgs = await resolveCallArgs(opts);
|
|
408
464
|
const token = await ensureCognitoIdToken();
|
|
409
465
|
const companyUid = await getCompanyUid(token, opts.company);
|
|
410
466
|
const connection = selectConnection(await fetchConnections(token, companyUid), opts);
|
|
@@ -417,7 +473,7 @@ export function registerIntegrationsCommand(program) {
|
|
|
417
473
|
// arg with a routing name would redirect the call; refuse it.
|
|
418
474
|
const reserved = BUILT_IN_ROUTING_ARGS.filter((key) => key in parsedArgs);
|
|
419
475
|
if (reserved.length) {
|
|
420
|
-
throw new IntegrationsCliError(
|
|
476
|
+
throw new IntegrationsCliError(`Tool arguments cannot include ${reserved.join(", ")}: it is set by hq from the selected connection.`, { expected: true });
|
|
421
477
|
}
|
|
422
478
|
gatewayParams = {
|
|
423
479
|
companyUid,
|
|
@@ -525,16 +581,41 @@ export function registerIntegrationsCommand(program) {
|
|
|
525
581
|
process.exitCode = 1;
|
|
526
582
|
return;
|
|
527
583
|
}
|
|
584
|
+
if (decision === "reject") {
|
|
585
|
+
if (opts.json)
|
|
586
|
+
printJson(body);
|
|
587
|
+
else
|
|
588
|
+
console.log(chalk.yellow(`Rejected (status: ${body.status ?? "rejected"}).`));
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
// A synchronous approve runs the call inline. The gateway answers a
|
|
592
|
+
// provider-side tool error with { status: "rejected", outcome:
|
|
593
|
+
// "failed", rejectionReason: "provider_tool_error" }; an older
|
|
594
|
+
// gateway reports "resumed" with an isError result. Classify both as
|
|
595
|
+
// failures, the same way the polling path does.
|
|
596
|
+
const result = body.result === undefined ? undefined : unwrapGatewayResult(body.result);
|
|
597
|
+
const toolError = body.result !== undefined &&
|
|
598
|
+
(gatewayResultIsError(result) || gatewayResultIsError(body.result));
|
|
599
|
+
const failed = approvalOutcomeFailed(body) || toolError;
|
|
600
|
+
if (failed)
|
|
601
|
+
process.exitCode = 1;
|
|
528
602
|
if (opts.json) {
|
|
529
603
|
printJson(body);
|
|
530
604
|
return;
|
|
531
605
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
printJson(unwrapGatewayResult(body.result));
|
|
606
|
+
if (failed) {
|
|
607
|
+
const reason = body.rejectionReason ??
|
|
608
|
+
(toolError ? "provider_tool_error" : body.outcome ?? body.status ?? "failed");
|
|
609
|
+
console.log(chalk.red(`The approved call failed at the provider (${reason}).`));
|
|
537
610
|
}
|
|
611
|
+
else if (body.status === "resumed" || body.status === "completed" || body.outcome === "ran") {
|
|
612
|
+
console.log(chalk.green(`Approved — the call ran (status: ${body.status ?? body.outcome}).`));
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
console.log(chalk.yellow(`Approval ended with status ${body.status ?? "unknown"}.`));
|
|
616
|
+
}
|
|
617
|
+
if (result !== undefined)
|
|
618
|
+
printJson(result);
|
|
538
619
|
});
|
|
539
620
|
}
|
|
540
621
|
registerConnectCommands(integrations);
|
|
@@ -18,9 +18,18 @@ export interface LoopControllerDeps {
|
|
|
18
18
|
resumeLane(laneId: string, options: LaneResumeOptions): Promise<unknown>;
|
|
19
19
|
interruptLane(laneId: string, reason: string, options: {
|
|
20
20
|
preserveLoopQueue: boolean;
|
|
21
|
+
terminalReason?: string;
|
|
21
22
|
}): Promise<void>;
|
|
22
23
|
emitTerminal(lane: LaneRecord): Promise<void>;
|
|
23
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* terminal_reason values for interrupts HQ issues itself. An operator
|
|
27
|
+
* interrupt (`hq lanes interrupt`) records `operator_interrupt`; these keep a
|
|
28
|
+
* system recovery from looking like an operator action in the lane record.
|
|
29
|
+
*/
|
|
30
|
+
export declare const LOOP_CONTROLLER_RECOVERY_REASON = "loop_controller_recovery";
|
|
31
|
+
export declare const LOOP_STALL_RESTART_REASON = "loop_stall_restart";
|
|
32
|
+
export declare const LOOP_PHASE_DEADLINE_REASON = "loop_phase_deadline";
|
|
24
33
|
export declare function startLaneLoopController(ctx: Ctx, laneId: string, initialAdmission?: boolean): number;
|
|
25
34
|
export declare function runLaneLoopController(ctx: Ctx, laneId: string, deps: LoopControllerDeps): Promise<void>;
|
|
26
35
|
//# sourceMappingURL=lanes-loop.d.ts.map
|
|
@@ -52,6 +52,14 @@ function casLoopLaneState(hqRoot, laneId, from, to) {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* terminal_reason values for interrupts HQ issues itself. An operator
|
|
57
|
+
* interrupt (`hq lanes interrupt`) records `operator_interrupt`; these keep a
|
|
58
|
+
* system recovery from looking like an operator action in the lane record.
|
|
59
|
+
*/
|
|
60
|
+
export const LOOP_CONTROLLER_RECOVERY_REASON = "loop_controller_recovery";
|
|
61
|
+
export const LOOP_STALL_RESTART_REASON = "loop_stall_restart";
|
|
62
|
+
export const LOOP_PHASE_DEADLINE_REASON = "loop_phase_deadline";
|
|
55
63
|
export function startLaneLoopController(ctx, laneId, initialAdmission = false) {
|
|
56
64
|
const entry = process.argv[1];
|
|
57
65
|
if (!entry)
|
|
@@ -202,7 +210,7 @@ export async function runLaneLoopController(ctx, laneId, deps) {
|
|
|
202
210
|
}
|
|
203
211
|
let interruptPromise;
|
|
204
212
|
const abortPhase = () => {
|
|
205
|
-
interruptPromise = deps.interruptLane(laneId, "loop phase deadline exceeded", { preserveLoopQueue: true }).catch((error) => {
|
|
213
|
+
interruptPromise = deps.interruptLane(laneId, "loop phase deadline exceeded", { preserveLoopQueue: true, terminalReason: LOOP_PHASE_DEADLINE_REASON }).catch((error) => {
|
|
206
214
|
process.stderr.write(`hq lanes: could not interrupt stalled loop phase ${laneId}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
207
215
|
});
|
|
208
216
|
};
|
package/dist/commands/lanes.js
CHANGED
|
@@ -48,7 +48,7 @@ import { appendLaneFocusOverride, assertFocusGuardAllows, makeFocusOverride } fr
|
|
|
48
48
|
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, LANE_CONDUCT_LOOP_FLAG, LANE_ENVELOPE_REPAIR_GUIDANCE_FLAG, } from "../lib/lanes/flag-keys.js";
|
|
49
49
|
import { applyContinuitySettings, findWorkerContinuityLane, sameLaneHost, workerContinuityPrompt } from "../lib/lanes/worker-continuity.js";
|
|
50
50
|
import { commitIdleLoopStop, enqueueLoopEnvelope, interruptLoopQueue, interruptLoopQueueForStory, loopControllerIsAlive, loopLaneIsIdle, loopStatus, validateLoopEnvelope } from "../lib/lanes/loop.js";
|
|
51
|
-
import { runLaneLoopController, startLaneLoopController } from "./lanes-loop.js";
|
|
51
|
+
import { LOOP_CONTROLLER_RECOVERY_REASON, LOOP_STALL_RESTART_REASON, runLaneLoopController, startLaneLoopController } from "./lanes-loop.js";
|
|
52
52
|
import { configuredCodexRolloutHomes, retainCodexRollouts } from "../lib/lanes/disk-guard.js";
|
|
53
53
|
import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
|
|
54
54
|
import { runLeadAutopilotPass } from "../lib/lanes/autopilot.js";
|
|
@@ -494,6 +494,41 @@ function livenessDepsFromCtx(ctx) {
|
|
|
494
494
|
hostname: ctx.hostname,
|
|
495
495
|
};
|
|
496
496
|
}
|
|
497
|
+
/**
|
|
498
|
+
* Start a lane that admission just let through. A queued loop lane is marked
|
|
499
|
+
* running and gets its loop controller, which works through the queued phase
|
|
500
|
+
* envelopes; every other lane spawns its worker directly. Every admission path
|
|
501
|
+
* (reconcile, in-process queue drain, hq lanes admit) goes through here so a
|
|
502
|
+
* loop lane never runs its brief without a controller.
|
|
503
|
+
*/
|
|
504
|
+
function startAdmittedLane(ctx, root, candidate) {
|
|
505
|
+
if (candidate.loop_mode !== true || candidate.state !== "queued") {
|
|
506
|
+
const admissionHost = reconcileHostFromCtx(ctx);
|
|
507
|
+
return spawnLaneWorker(root, candidate, {
|
|
508
|
+
env: ctx.env,
|
|
509
|
+
hostname: admissionHost.hostname,
|
|
510
|
+
admissionHost,
|
|
511
|
+
modelDeps: { readFileSync: ctx.readFileSync, probe: ctx.probeProvider, env: ctx.env },
|
|
512
|
+
existsSync: ctx.existsSync,
|
|
513
|
+
spawnProcess: ctx.spawnProcess,
|
|
514
|
+
proveDetached: ctx.proveDetached,
|
|
515
|
+
killProcess: ctx.killProcess,
|
|
516
|
+
now: ctx.now,
|
|
517
|
+
resolveMachineIdentity: ctx.resolveMachineIdentity,
|
|
518
|
+
runCompaction: ctx.runCompaction,
|
|
519
|
+
locateTranscript: ctx.locateTranscript,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
const admitted = casUpdateLane(root, candidate.lane_id, (current) => ({
|
|
523
|
+
...current,
|
|
524
|
+
state: "running",
|
|
525
|
+
queued_reason: undefined,
|
|
526
|
+
queued_details: undefined,
|
|
527
|
+
admission_reserved_at: undefined,
|
|
528
|
+
}));
|
|
529
|
+
startLaneLoopController(ctx, candidate.lane_id, true);
|
|
530
|
+
return admitted;
|
|
531
|
+
}
|
|
497
532
|
/**
|
|
498
533
|
* Admit queued lanes in-process after a command frees or adds capacity, so
|
|
499
534
|
* the queue drains without `hq daemon`. Best effort: an admission failure is
|
|
@@ -516,6 +551,7 @@ async function admitQueuedFromCtx(ctx) {
|
|
|
516
551
|
runCompaction: ctx.runCompaction,
|
|
517
552
|
locateTranscript: ctx.locateTranscript,
|
|
518
553
|
},
|
|
554
|
+
spawn: (root, candidate) => startAdmittedLane(ctx, root, candidate),
|
|
519
555
|
throttleDeps: ctx.throttleDeps,
|
|
520
556
|
livenessDeps: livenessDepsFromCtx(ctx),
|
|
521
557
|
presenceConnected: ctx.presenceConnected,
|
|
@@ -3355,7 +3391,7 @@ async function runCreate(ctx, opts) {
|
|
|
3355
3391
|
const requestedHireGrant = opts.hireGrant === undefined
|
|
3356
3392
|
? undefined
|
|
3357
3393
|
: parseHireGrant(opts.hireGrant);
|
|
3358
|
-
|
|
3394
|
+
let requestedPlacement = opts.placement !== undefined
|
|
3359
3395
|
? parsePlacementPolicy(opts.placement)
|
|
3360
3396
|
: undefined;
|
|
3361
3397
|
const defaultPlacement = opts.defaultPlacement !== undefined
|
|
@@ -3575,6 +3611,13 @@ async function runCreate(ctx, opts) {
|
|
|
3575
3611
|
placementGate ??= readLanePlacementEnabled(ctx, resolvePlacementCompany());
|
|
3576
3612
|
return placementGate;
|
|
3577
3613
|
};
|
|
3614
|
+
if (requestedPlacement?.kind === "local" &&
|
|
3615
|
+
defaultPlacement === undefined &&
|
|
3616
|
+
!(await placementEnabled())) {
|
|
3617
|
+
// With placement off every lane already runs here, so --placement local
|
|
3618
|
+
// asks for the default. Treat it as omitted instead of refusing.
|
|
3619
|
+
requestedPlacement = undefined;
|
|
3620
|
+
}
|
|
3578
3621
|
if (requestedPlacement !== undefined || defaultPlacement !== undefined) {
|
|
3579
3622
|
await assertPlacementEnabled(ctx, resolvePlacementCompany(), placementEnabled);
|
|
3580
3623
|
}
|
|
@@ -7159,22 +7202,7 @@ async function runAdmit(ctx, laneId, opts) {
|
|
|
7159
7202
|
});
|
|
7160
7203
|
return;
|
|
7161
7204
|
}
|
|
7162
|
-
const started =
|
|
7163
|
-
env: ctx.env,
|
|
7164
|
-
modelDeps: {
|
|
7165
|
-
readFileSync: ctx.readFileSync,
|
|
7166
|
-
probe: ctx.probeProvider,
|
|
7167
|
-
env: ctx.env,
|
|
7168
|
-
},
|
|
7169
|
-
existsSync: ctx.existsSync,
|
|
7170
|
-
spawnProcess: ctx.spawnProcess,
|
|
7171
|
-
proveDetached: ctx.proveDetached,
|
|
7172
|
-
killProcess: ctx.killProcess,
|
|
7173
|
-
now: ctx.now,
|
|
7174
|
-
resolveMachineIdentity: ctx.resolveMachineIdentity,
|
|
7175
|
-
runCompaction: ctx.runCompaction,
|
|
7176
|
-
locateTranscript: ctx.locateTranscript,
|
|
7177
|
-
});
|
|
7205
|
+
const started = startAdmittedLane(ctx, hqRoot, lane);
|
|
7178
7206
|
const admitted = opts.force && started.state === "running"
|
|
7179
7207
|
? casUpdateLane(hqRoot, laneId, (current) => ({ ...current, admitted_reason: "operator_force" }))
|
|
7180
7208
|
: started;
|
|
@@ -8811,7 +8839,7 @@ async function runStop(ctx, laneId, reason, quiet = false) {
|
|
|
8811
8839
|
}
|
|
8812
8840
|
const controllerAlive = loopControllerIsAlive(runDirPath);
|
|
8813
8841
|
if (!controllerAlive && lane.state === "running") {
|
|
8814
|
-
await runInterrupt(ctx, lane.lane_id, "loop controller exited; recovering queued work before stop", { preserveLoopQueue: true, quiet: true });
|
|
8842
|
+
await runInterrupt(ctx, lane.lane_id, "loop controller exited; recovering queued work before stop", { preserveLoopQueue: true, quiet: true, terminalReason: LOOP_CONTROLLER_RECOVERY_REASON });
|
|
8815
8843
|
}
|
|
8816
8844
|
const queued = enqueueLoopEnvelope({ runDir: runDirPath, laneId: lane.lane_id, envelope: { kind: "stop" }, now: ctx.now });
|
|
8817
8845
|
if (!controllerAlive && lane.state !== "queued")
|
|
@@ -8887,6 +8915,13 @@ function stopTerminalWarning(laneId) {
|
|
|
8887
8915
|
* session so `hq lanes resume` continues it. Keep-alive resumes only
|
|
8888
8916
|
* `exited_with_partial_output` interruptions, so it leaves this lane alone.
|
|
8889
8917
|
*/
|
|
8918
|
+
/** Stop-history text for an operator interrupt given without --reason. */
|
|
8919
|
+
function defaultOperatorInterruptReason(ctx) {
|
|
8920
|
+
const sessionId = sessionIdFromEnv(ctx.env);
|
|
8921
|
+
const laneId = ctx.env.HQ_LANE_ID?.trim();
|
|
8922
|
+
const caller = laneId ? ` by lane ${laneId}` : sessionId ? ` by session ${sessionId}` : "";
|
|
8923
|
+
return `hq lanes interrupt${caller} (no reason given)`;
|
|
8924
|
+
}
|
|
8890
8925
|
async function runInterrupt(ctx, laneId, reason, options = {}) {
|
|
8891
8926
|
const operatorReason = reason?.trim();
|
|
8892
8927
|
if (reason !== undefined && !operatorReason) {
|
|
@@ -8894,6 +8929,11 @@ async function runInterrupt(ctx, laneId, reason, options = {}) {
|
|
|
8894
8929
|
}
|
|
8895
8930
|
if (operatorReason !== undefined)
|
|
8896
8931
|
assertNoSecretText("interrupt reason", operatorReason);
|
|
8932
|
+
const terminalReason = options.terminalReason ?? "operator_interrupt";
|
|
8933
|
+
// Every interrupt leaves a stop-history entry, so an operator interrupt
|
|
8934
|
+
// given without --reason is still distinguishable from a system one.
|
|
8935
|
+
const historyReason = operatorReason ??
|
|
8936
|
+
(terminalReason === "operator_interrupt" ? defaultOperatorInterruptReason(ctx) : terminalReason);
|
|
8897
8937
|
const hqRoot = ctx.hqRoot();
|
|
8898
8938
|
const lane = readLane(hqRoot, laneId);
|
|
8899
8939
|
if (lane.host.kind === "outpost" && !laneRunsHereForOperatorMutation(ctx, lane)) {
|
|
@@ -8918,7 +8958,7 @@ async function runInterrupt(ctx, laneId, reason, options = {}) {
|
|
|
8918
8958
|
if (!reason)
|
|
8919
8959
|
reason = "story-scoped loop phase interrupt";
|
|
8920
8960
|
const remaining = scoped;
|
|
8921
|
-
await runInterrupt(ctx, laneId, reason, { preserveLoopQueue: true, quiet: true });
|
|
8961
|
+
await runInterrupt(ctx, laneId, reason, { preserveLoopQueue: true, quiet: true, terminalReason: options.terminalReason });
|
|
8922
8962
|
if (!options.quiet)
|
|
8923
8963
|
printJson({ ok: true, action: "interrupt", lane_id: lane.lane_id, terminal: false, resumable: true, ...remaining });
|
|
8924
8964
|
return;
|
|
@@ -8953,13 +8993,11 @@ async function runInterrupt(ctx, laneId, reason, options = {}) {
|
|
|
8953
8993
|
queued_reason: undefined,
|
|
8954
8994
|
queued_details: undefined,
|
|
8955
8995
|
interrupted_at: at,
|
|
8956
|
-
terminal_reason:
|
|
8957
|
-
|
|
8958
|
-
stop_history
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
],
|
|
8962
|
-
}),
|
|
8996
|
+
terminal_reason: terminalReason,
|
|
8997
|
+
stop_history: [
|
|
8998
|
+
...(current.stop_history ?? []),
|
|
8999
|
+
{ at, reason: historyReason },
|
|
9000
|
+
],
|
|
8963
9001
|
};
|
|
8964
9002
|
});
|
|
8965
9003
|
stopLaneProcesses(hqRoot, laneId, {
|
|
@@ -9315,31 +9353,7 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
|
|
|
9315
9353
|
runCompaction: ctx.runCompaction,
|
|
9316
9354
|
locateTranscript: ctx.locateTranscript,
|
|
9317
9355
|
},
|
|
9318
|
-
spawn: (root, candidate) =>
|
|
9319
|
-
if (candidate.loop_mode !== true || candidate.state !== "queued") {
|
|
9320
|
-
return spawnLaneWorker(root, candidate, {
|
|
9321
|
-
env: ctx.env,
|
|
9322
|
-
modelDeps: { readFileSync: ctx.readFileSync, probe: ctx.probeProvider, env: ctx.env },
|
|
9323
|
-
existsSync: ctx.existsSync,
|
|
9324
|
-
spawnProcess: ctx.spawnProcess,
|
|
9325
|
-
proveDetached: ctx.proveDetached,
|
|
9326
|
-
killProcess: ctx.killProcess,
|
|
9327
|
-
now: ctx.now,
|
|
9328
|
-
resolveMachineIdentity: ctx.resolveMachineIdentity,
|
|
9329
|
-
runCompaction: ctx.runCompaction,
|
|
9330
|
-
locateTranscript: ctx.locateTranscript,
|
|
9331
|
-
});
|
|
9332
|
-
}
|
|
9333
|
-
const admitted = casUpdateLane(root, candidate.lane_id, (current) => ({
|
|
9334
|
-
...current,
|
|
9335
|
-
state: "running",
|
|
9336
|
-
queued_reason: undefined,
|
|
9337
|
-
queued_details: undefined,
|
|
9338
|
-
admission_reserved_at: undefined,
|
|
9339
|
-
}));
|
|
9340
|
-
startLaneLoopController(ctx, candidate.lane_id, true);
|
|
9341
|
-
return admitted;
|
|
9342
|
-
},
|
|
9356
|
+
spawn: (root, candidate) => startAdmittedLane(ctx, root, candidate),
|
|
9343
9357
|
throttleDeps: ctx.throttleDeps,
|
|
9344
9358
|
livenessDeps: livenessDepsFromCtx(ctx),
|
|
9345
9359
|
presenceConnected: ctx.presenceConnected,
|
|
@@ -9701,7 +9715,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9701
9715
|
return deps.restartLoopController(handlerCtx, laneId);
|
|
9702
9716
|
const current = readLane(handlerCtx.hqRoot(), laneId);
|
|
9703
9717
|
if (current.state === "running") {
|
|
9704
|
-
await runInterrupt(handlerCtx, laneId, "loop stall question answered; restarting controller", { preserveLoopQueue: true, quiet: true });
|
|
9718
|
+
await runInterrupt(handlerCtx, laneId, "loop stall question answered; restarting controller", { preserveLoopQueue: true, quiet: true, terminalReason: LOOP_STALL_RESTART_REASON });
|
|
9705
9719
|
}
|
|
9706
9720
|
const runDirPath = runDir(handlerCtx.hqRoot(), laneId);
|
|
9707
9721
|
if (loopControllerIsAlive(runDirPath)) {
|
|
@@ -9991,7 +10005,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9991
10005
|
const controllerAlive = loopControllerIsAlive(runDirPath);
|
|
9992
10006
|
const result = enqueueLoopEnvelope({ runDir: runDirPath, laneId: lane.lane_id, envelope, now: ctx.now });
|
|
9993
10007
|
if (!controllerAlive && lane.state === "running") {
|
|
9994
|
-
await runInterrupt(ctx, lane.lane_id, "loop controller exited; recovering the queued phase", { preserveLoopQueue: true, quiet: true });
|
|
10008
|
+
await runInterrupt(ctx, lane.lane_id, "loop controller exited; recovering the queued phase", { preserveLoopQueue: true, quiet: true, terminalReason: LOOP_CONTROLLER_RECOVERY_REASON });
|
|
9995
10009
|
}
|
|
9996
10010
|
const shouldRestart = !controllerAlive && lane.state !== "queued";
|
|
9997
10011
|
const controllerPid = shouldRestart ? startLaneLoopController(ctx, lane.lane_id) : undefined;
|
|
@@ -10386,6 +10400,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
10386
10400
|
}));
|
|
10387
10401
|
lanes
|
|
10388
10402
|
.command("stop")
|
|
10403
|
+
.alias("cancel")
|
|
10389
10404
|
.description("Cancel a queued lane or kill a running one (terminal; use interrupt to keep it resumable)")
|
|
10390
10405
|
.argument("<lane-id>", "Lane id")
|
|
10391
10406
|
.option("--reason <text>", "Record the reason in the lane's terminal history")
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
/** Upper bound on list pages one short-id lookup reads (per cursor chain). */
|
|
3
|
+
export declare const SHORT_ID_MAX_PAGES = 50;
|
|
2
4
|
export declare function registerMeetingsCommand(program: Command): void;
|
|
3
5
|
//# sourceMappingURL=meetings.d.ts.map
|