@indigoai-us/hq-cli 5.345.57 → 5.345.59
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 +12 -0
- package/dist/command-catalog.generated.js +15 -0
- package/dist/commands/access.d.ts +14 -0
- package/dist/commands/access.js +69 -1
- package/dist/commands/agent-kit.js +20 -10
- package/dist/commands/billing.d.ts +13 -0
- package/dist/commands/billing.js +31 -0
- package/dist/commands/db-dump.js +2 -2
- package/dist/commands/db-remote-deps.js +6 -0
- package/dist/commands/lanes-loop.js +3 -1
- package/dist/commands/lanes-questions.js +16 -0
- package/dist/commands/lanes-session-start.js +19 -3
- package/dist/commands/lanes-shared.d.ts +1 -0
- package/dist/commands/lanes.d.ts +3 -1
- package/dist/commands/lanes.js +52 -2
- package/dist/lib/agent-kit/inbox-poll-backoff-flag.d.ts +19 -0
- package/dist/lib/agent-kit/inbox-poll-backoff-flag.js +70 -0
- package/dist/lib/agent-kit/run/inbox.d.ts +7 -3
- package/dist/lib/agent-kit/run/inbox.js +45 -6
- package/dist/lib/agent-kit/run/stop-signals.d.ts +14 -0
- package/dist/lib/agent-kit/run/stop-signals.js +26 -0
- package/dist/lib/agent-kit/services.d.ts +2 -2
- package/dist/lib/agent-kit/services.js +5 -3
- package/dist/lib/anywhere-runtime-flag-key.d.ts +1 -0
- package/dist/lib/anywhere-runtime-flag-key.js +1 -0
- package/dist/lib/anywhere-runtime-revocation-marker.d.ts +4 -0
- package/dist/lib/anywhere-runtime-revocation-marker.js +26 -0
- package/dist/lib/daemon/run.d.ts +2 -0
- package/dist/lib/daemon/run.js +17 -4
- package/dist/lib/daemon/server.js +7 -0
- package/dist/lib/db/control-plane.d.ts +12 -1
- package/dist/lib/db/usage-format.d.ts +2 -1
- package/dist/lib/db/usage-format.js +19 -0
- package/dist/lib/install/provider-wrappers.js +13 -0
- package/dist/lib/lanes/loop.d.ts +23 -1
- package/dist/lib/lanes/loop.js +134 -19
- package/dist/lib/lanes/questions.d.ts +5 -0
- package/dist/lib/lanes/reconcile.js +3 -0
- package/dist/lib/lanes/story-ledger.js +30 -20
- package/dist/lib/lanes/types.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.59] — 2026-10-09
|
|
6
|
+
- Keep conduct-mode SessionStart injection out of unattended sessions unless `HQ_AUTO_CONDUCT` explicitly forces it on.
|
|
7
|
+
- Back off the external agent-kit inbox poller from 15 seconds to 30 seconds after repeated empty polls, reset to 15 seconds when a message is mirrored, and stop promptly on SIGTERM or SIGINT with a 10-second deadline for stalled shutdowns. The default-on `agents.inbox-poll-backoff-agent-kit` hq-flags kill switch preserves the regular cadence when disabled; installed services retain a configured `HQ_FLAGS_API_URL`.
|
|
8
|
+
|
|
9
|
+
- Fix loop-lane recovery: retries receive the fixed per-attempt phase budget; withdrawn and stalled IDs can be reused; `interrupt --story/--phase` targets matching work; and stall answers resume through the controller.
|
|
10
|
+
- `hq db usage --app` says when a deleted app's database was removed, how long its data is kept, and that it is not billed, instead of showing usage and an estimate.
|
|
11
|
+
- `hq db sql --app` and `hq db dump --app` say a deleted app's database was removed, or that the app has no database, instead of "not available yet".
|
|
12
|
+
- `hq billing status` lists database apps: how many are active, the $10 monthly price, overage per app this month, whether the company is exempt, and which subscription carries the charge.
|
|
13
|
+
- Installed Claude and Codex provider wrappers now bypass HQ when the daemon's refreshed runtime flag or person setting is revoked, using a per-user marker under the HQ registry directory.
|
|
14
|
+
- Lane updates preserve completed stories and lead-set states while applying lane progress in order.
|
|
15
|
+
- `hq lanes resume --story` can clear a prior unacknowledged story-switch note.
|
|
16
|
+
|
|
17
|
+
## [5.345.58] — 2026-10-09
|
|
18
|
+
- hq access now accepts `--as <member>` to view another member's access, read-only.
|
|
19
|
+
|
|
5
20
|
## [5.345.57] — 2026-10-09
|
|
6
21
|
- Add `hq lanes create --loop` and `hq lanes enqueue` for persistent worker lanes that process FIFO phase envelopes, return generic results or section 8 handoffs, reuse sessions per story, retry one stall, and park a second stall for the senior. Loop behavior uses the default-on `lanes.conduct-loop` hq-flags kill switch. See `docs/lanes.md` for command output and exit codes.
|
|
7
22
|
|
|
@@ -2871,6 +2871,10 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
2871
2871
|
readonly flags: "--company <slug>";
|
|
2872
2872
|
readonly description: "Company slug";
|
|
2873
2873
|
readonly hidden: false;
|
|
2874
|
+
}, {
|
|
2875
|
+
readonly flags: "--as <person-uid>";
|
|
2876
|
+
readonly description: "Check a person's access (exact prs_ or agt_ UID; self or owner/admin)";
|
|
2877
|
+
readonly hidden: false;
|
|
2874
2878
|
}, {
|
|
2875
2879
|
readonly flags: "--json";
|
|
2876
2880
|
readonly description: "Print a single JSON object";
|
|
@@ -12931,6 +12935,14 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
12931
12935
|
readonly flags: "--reason <text>";
|
|
12932
12936
|
readonly description: "Record the reason in the lane's stop history";
|
|
12933
12937
|
readonly hidden: false;
|
|
12938
|
+
}, {
|
|
12939
|
+
readonly flags: "--story <id>";
|
|
12940
|
+
readonly description: "Withdraw only this story's queued phases (loop lanes only)";
|
|
12941
|
+
readonly hidden: false;
|
|
12942
|
+
}, {
|
|
12943
|
+
readonly flags: "--phase <name>";
|
|
12944
|
+
readonly description: "Limit --story interruption to one phase";
|
|
12945
|
+
readonly hidden: false;
|
|
12934
12946
|
}, {
|
|
12935
12947
|
readonly flags: "--json";
|
|
12936
12948
|
readonly description: "JSON on stdout (always)";
|
|
@@ -3462,6 +3462,11 @@ export const COMMAND_CATALOG = [
|
|
|
3462
3462
|
"description": "Company slug",
|
|
3463
3463
|
"hidden": false
|
|
3464
3464
|
},
|
|
3465
|
+
{
|
|
3466
|
+
"flags": "--as <person-uid>",
|
|
3467
|
+
"description": "Check a person's access (exact prs_ or agt_ UID; self or owner/admin)",
|
|
3468
|
+
"hidden": false
|
|
3469
|
+
},
|
|
3465
3470
|
{
|
|
3466
3471
|
"flags": "--json",
|
|
3467
3472
|
"description": "Print a single JSON object",
|
|
@@ -15736,6 +15741,16 @@ export const COMMAND_CATALOG = [
|
|
|
15736
15741
|
"description": "Record the reason in the lane's stop history",
|
|
15737
15742
|
"hidden": false
|
|
15738
15743
|
},
|
|
15744
|
+
{
|
|
15745
|
+
"flags": "--story <id>",
|
|
15746
|
+
"description": "Withdraw only this story's queued phases (loop lanes only)",
|
|
15747
|
+
"hidden": false
|
|
15748
|
+
},
|
|
15749
|
+
{
|
|
15750
|
+
"flags": "--phase <name>",
|
|
15751
|
+
"description": "Limit --story interruption to one phase",
|
|
15752
|
+
"hidden": false
|
|
15753
|
+
},
|
|
15739
15754
|
{
|
|
15740
15755
|
"flags": "--json",
|
|
15741
15756
|
"description": "JSON on stdout (always)",
|
|
@@ -135,6 +135,15 @@ export interface RunAccessResult {
|
|
|
135
135
|
alreadyAskedAt?: string;
|
|
136
136
|
requestNote?: string;
|
|
137
137
|
}
|
|
138
|
+
export interface AccessAsResult {
|
|
139
|
+
personUid: string;
|
|
140
|
+
company: string;
|
|
141
|
+
path: string;
|
|
142
|
+
permission: "read" | "write" | "admin" | null;
|
|
143
|
+
canRead: boolean;
|
|
144
|
+
roleBypass: boolean;
|
|
145
|
+
exitCode: 0 | 3;
|
|
146
|
+
}
|
|
138
147
|
export declare function outcomeExitCode(outcome: AccessOutcome): 0 | 2 | 3 | 4;
|
|
139
148
|
export declare function formatAccessOutcome(result: RunAccessResult): string;
|
|
140
149
|
export declare function resolveGrantor(tree: AccessAclTree | null, members: AccessMember[]): AccessGrantor | null;
|
|
@@ -149,6 +158,11 @@ export declare function resolveAccessCompany(target: string, opts?: {
|
|
|
149
158
|
company?: string;
|
|
150
159
|
hqRoot?: string;
|
|
151
160
|
}): string | undefined;
|
|
161
|
+
export declare function runAccessForPerson(rawPath: string, personUid: string, opts?: {
|
|
162
|
+
company?: string;
|
|
163
|
+
hqRoot?: string;
|
|
164
|
+
}): Promise<AccessAsResult>;
|
|
165
|
+
export declare function formatAccessForPerson(result: AccessAsResult): string;
|
|
152
166
|
export declare function runAccessForPath(path: string, opts?: {
|
|
153
167
|
company?: string;
|
|
154
168
|
hqRoot?: string;
|
package/dist/commands/access.js
CHANGED
|
@@ -20,7 +20,7 @@ import * as readline from "node:readline";
|
|
|
20
20
|
import { ListObjectsV2Command, } from "@aws-sdk/client-s3";
|
|
21
21
|
import { DEFAULT_HQ_ROOT } from "../utils/cognito-session.js";
|
|
22
22
|
import { getCompanyUid, vaultApiFetch, resolveCallerUid } from "../utils/vault-api.js";
|
|
23
|
-
import { parseCompanySlugFromPath, resolveBrowseSession, makeCompanyPresignFactory, runGet, } from "./files-browse.js";
|
|
23
|
+
import { parseCompanySlugFromPath, classifyAclSource, resolveBrowseSession, makeCompanyPresignFactory, runGet, toBucketRelative, } from "./files-browse.js";
|
|
24
24
|
import { listJournals, syncDoctor, } from "@indigoai-us/hq-cloud";
|
|
25
25
|
import { listActiveMembers } from "./members.js";
|
|
26
26
|
import { buildDmBody } from "./dm.js";
|
|
@@ -87,6 +87,10 @@ function normalizeTarget(raw, slug) {
|
|
|
87
87
|
t = t.slice(anchor.length);
|
|
88
88
|
return t;
|
|
89
89
|
}
|
|
90
|
+
function fileGrantCoversPath(grantPath, targetPath) {
|
|
91
|
+
return classifyAclSource(targetPath, [grantPath]) !== "role-bypass";
|
|
92
|
+
}
|
|
93
|
+
const FILE_PERMISSION_RANK = { read: 1, write: 2, admin: 3 };
|
|
90
94
|
function httpStatusOf(err) {
|
|
91
95
|
if (!err || typeof err !== "object")
|
|
92
96
|
return undefined;
|
|
@@ -637,6 +641,7 @@ export function registerAccessCommand(program) {
|
|
|
637
641
|
.command("access <path-or-query>")
|
|
638
642
|
.description("Tell whether a vault file exists and whether you can read it. Never a bare not-found. Exit codes: 0 local/not-synced, 2 never-existed, 3 no-access, 4 ambiguous.")
|
|
639
643
|
.option("--company <slug>", "Company slug")
|
|
644
|
+
.option("--as <person-uid>", "Check a person's access (exact prs_ or agt_ UID; self or owner/admin)")
|
|
640
645
|
.option("--json", "Print a single JSON object")
|
|
641
646
|
.option("--no-fix", "Diagnose only: do not fetch, pin, or repair sync")
|
|
642
647
|
.option("--yes", "Answer the one send-confirmation yes (hosts must pass this explicitly)")
|
|
@@ -650,6 +655,12 @@ Exit codes:
|
|
|
650
655
|
`)
|
|
651
656
|
.action(async (target, opts) => {
|
|
652
657
|
try {
|
|
658
|
+
if (opts.as) {
|
|
659
|
+
const result = await runAccessForPerson(target, opts.as, opts);
|
|
660
|
+
console.log(opts.json ? JSON.stringify(result) : formatAccessForPerson(result));
|
|
661
|
+
process.exitCode = result.exitCode;
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
653
664
|
const result = await runAccessForPath(target, opts);
|
|
654
665
|
process.exitCode = result.exitCode;
|
|
655
666
|
}
|
|
@@ -669,6 +680,63 @@ export function resolveAccessCompany(target, opts) {
|
|
|
669
680
|
const hqRoot = opts?.hqRoot ?? DEFAULT_HQ_ROOT;
|
|
670
681
|
return opts?.company ?? slugFromTarget(target) ?? readActiveCompany(hqRoot);
|
|
671
682
|
}
|
|
683
|
+
export async function runAccessForPerson(rawPath, personUid, opts) {
|
|
684
|
+
const uid = personUid.trim();
|
|
685
|
+
if (!/^(?:prs|agt)_[A-Za-z0-9_-]+$/.test(uid)) {
|
|
686
|
+
throw new Error("Pass the person's exact prs_ or agt_ UID to --as.");
|
|
687
|
+
}
|
|
688
|
+
const pathSlug = slugFromTarget(rawPath);
|
|
689
|
+
if (opts?.company && pathSlug && opts.company !== pathSlug) {
|
|
690
|
+
throw new Error("The --company value does not match the company in the path.");
|
|
691
|
+
}
|
|
692
|
+
const company = resolveAccessCompany(rawPath, opts);
|
|
693
|
+
if (!company)
|
|
694
|
+
throw new Error("Pass --company <slug> or an anchored company path.");
|
|
695
|
+
const relativePath = normalizeTarget(rawPath, company);
|
|
696
|
+
if (!relativePath || relativePath.split(/[\\/]/).includes("..")) {
|
|
697
|
+
throw new Error("Pass a company-relative path without parent-directory segments.");
|
|
698
|
+
}
|
|
699
|
+
const { token } = await resolveBrowseSession();
|
|
700
|
+
const companyUid = await getCompanyUid(token, company);
|
|
701
|
+
const response = await vaultApiFetch({
|
|
702
|
+
token,
|
|
703
|
+
path: `/files/${encodeURIComponent(companyUid)}/members/${encodeURIComponent(uid)}/access`,
|
|
704
|
+
method: "GET",
|
|
705
|
+
});
|
|
706
|
+
if (!response.ok) {
|
|
707
|
+
if (response.status === 403) {
|
|
708
|
+
throw new Error("Only the person themself or a company owner/admin can check this access.");
|
|
709
|
+
}
|
|
710
|
+
if (response.status === 404) {
|
|
711
|
+
throw new Error("That person is not an active member of this company.");
|
|
712
|
+
}
|
|
713
|
+
throw new Error(`Member access check failed (${response.status}).`);
|
|
714
|
+
}
|
|
715
|
+
const projection = (await response.json());
|
|
716
|
+
if (projection.personUid !== uid || !projection.files) {
|
|
717
|
+
throw new Error("The member access response did not match the requested person.");
|
|
718
|
+
}
|
|
719
|
+
const roleBypass = projection.files.roleBypass === true;
|
|
720
|
+
const grants = Array.isArray(projection.files.grants) ? projection.files.grants : [];
|
|
721
|
+
const matchingPermissions = grants
|
|
722
|
+
.filter((grant) => typeof grant.path === "string" && typeof grant.permission === "string" &&
|
|
723
|
+
grant.permission in FILE_PERMISSION_RANK &&
|
|
724
|
+
fileGrantCoversPath(toBucketRelative(grant.path, company), relativePath))
|
|
725
|
+
.map((grant) => grant.permission);
|
|
726
|
+
const permission = roleBypass ? "admin" : matchingPermissions.reduce((best, candidate) => best === null || FILE_PERMISSION_RANK[candidate] > FILE_PERMISSION_RANK[best] ? candidate : best, null);
|
|
727
|
+
return {
|
|
728
|
+
personUid: uid,
|
|
729
|
+
company,
|
|
730
|
+
path: `companies/${company}/${relativePath}`,
|
|
731
|
+
permission,
|
|
732
|
+
canRead: permission !== null,
|
|
733
|
+
roleBypass,
|
|
734
|
+
exitCode: permission === null ? 3 : 0,
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
export function formatAccessForPerson(result) {
|
|
738
|
+
return `${result.personUid} has ${result.permission ?? "no access"} access to ${result.path}.`;
|
|
739
|
+
}
|
|
672
740
|
export async function runAccessForPath(path, opts) {
|
|
673
741
|
const target = path;
|
|
674
742
|
const hqRoot = opts?.hqRoot ?? DEFAULT_HQ_ROOT;
|
|
@@ -30,6 +30,7 @@ import { isKitService, KIT_SERVICES, enabledKitServices, kitServiceSpec, kitServ
|
|
|
30
30
|
import { writeKitSkills } from "../lib/agent-kit/skills.js";
|
|
31
31
|
import { runHeartbeatLoop } from "../lib/agent-kit/run/heartbeat.js";
|
|
32
32
|
import { runInboxLoop } from "../lib/agent-kit/run/inbox.js";
|
|
33
|
+
import { createKitStopController } from "../lib/agent-kit/run/stop-signals.js";
|
|
33
34
|
import { superviseKitServices } from "../lib/agent-kit/run/supervisor.js";
|
|
34
35
|
import { fallbackStatus, rebootCrontabLine, ensureRebootCrontab, shouldUseFallback, startFallback, stopFallback, } from "../lib/agent-kit/fallback.js";
|
|
35
36
|
import { serviceLogPath } from "../lib/agent-kit/paths.js";
|
|
@@ -182,17 +183,26 @@ export async function runKitService(service, rt) {
|
|
|
182
183
|
credsSource: rt.credsSource ?? "default",
|
|
183
184
|
});
|
|
184
185
|
return;
|
|
185
|
-
case "inbox":
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
186
|
+
case "inbox": {
|
|
187
|
+
const stop = createKitStopController();
|
|
188
|
+
try {
|
|
189
|
+
await runInboxLoop({
|
|
190
|
+
paths: rt.paths,
|
|
191
|
+
agentUid: rt.creds.entityUid,
|
|
192
|
+
apiBaseUrl: rt.creds.apiBaseUrl,
|
|
193
|
+
pollMs: rt.config.inboxPollMs,
|
|
194
|
+
ack: rt.config.inboxAck,
|
|
195
|
+
getToken: rt.getToken,
|
|
196
|
+
log: rt.log,
|
|
197
|
+
companySlug: rt.creds.companySlug,
|
|
198
|
+
signal: stop.signal,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
stop.dispose();
|
|
203
|
+
}
|
|
195
204
|
return;
|
|
205
|
+
}
|
|
196
206
|
}
|
|
197
207
|
}
|
|
198
208
|
/** `kit run all`: supervise the kit services as child processes with restart backoff. */
|
|
@@ -16,5 +16,18 @@
|
|
|
16
16
|
* answer such a request
|
|
17
17
|
*/
|
|
18
18
|
import { Command } from "commander";
|
|
19
|
+
interface DatabaseBilling {
|
|
20
|
+
period: string;
|
|
21
|
+
activeApps: number;
|
|
22
|
+
pricePerAppCents: number;
|
|
23
|
+
exempt: boolean;
|
|
24
|
+
chargedOn: "team" | "dedicated" | "existing" | null;
|
|
25
|
+
apps: Array<{
|
|
26
|
+
appId: string;
|
|
27
|
+
overageCents: number;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
export declare function formatDatabaseBilling(db: DatabaseBilling | null | undefined): string[];
|
|
19
31
|
export declare function registerBillingCommand(program: Command): void;
|
|
32
|
+
export {};
|
|
20
33
|
//# sourceMappingURL=billing.d.ts.map
|
package/dist/commands/billing.js
CHANGED
|
@@ -21,6 +21,35 @@ import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
|
21
21
|
import { mintPaymentLink } from "../utils/billing-gate.js";
|
|
22
22
|
import { upgradeToTeam } from "../utils/team-upgrade.js";
|
|
23
23
|
import { registerBillingSponsorCommands } from "./billing-sponsor.js";
|
|
24
|
+
const CHARGED_ON = {
|
|
25
|
+
team: "the Team subscription",
|
|
26
|
+
dedicated: "the separate HQ database apps subscription",
|
|
27
|
+
existing: "the company's existing subscription",
|
|
28
|
+
};
|
|
29
|
+
function dollars(cents) {
|
|
30
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
31
|
+
}
|
|
32
|
+
export function formatDatabaseBilling(db) {
|
|
33
|
+
if (db === undefined)
|
|
34
|
+
return [];
|
|
35
|
+
if (db === null)
|
|
36
|
+
return [`${chalk.bold("Database apps")}: could not be read right now`];
|
|
37
|
+
const lines = [
|
|
38
|
+
`${chalk.bold("Database apps")}: ${db.activeApps} active, ${dollars(db.pricePerAppCents)} a month each`,
|
|
39
|
+
];
|
|
40
|
+
if (db.exempt) {
|
|
41
|
+
lines.push(" Not billed: this company is exempt from database app charges.");
|
|
42
|
+
}
|
|
43
|
+
else if (db.activeApps > 0) {
|
|
44
|
+
lines.push(db.chargedOn
|
|
45
|
+
? ` Charged on ${CHARGED_ON[db.chargedOn]}.`
|
|
46
|
+
: " Not on a subscription yet. The charge is added on the next billing sync.");
|
|
47
|
+
}
|
|
48
|
+
for (const app of db.apps) {
|
|
49
|
+
lines.push(` ${app.appId}: ${dollars(app.overageCents)} overage so far in ${db.period}`);
|
|
50
|
+
}
|
|
51
|
+
return lines;
|
|
52
|
+
}
|
|
24
53
|
function fail(err) {
|
|
25
54
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
26
55
|
process.exit(1);
|
|
@@ -69,6 +98,8 @@ export function registerBillingCommand(program) {
|
|
|
69
98
|
console.log(`${chalk.bold("Card")}: ${chalk.yellow("no card on file")} — ` +
|
|
70
99
|
`run \`hq billing checkout\` to add one before provisioning paid resources.`);
|
|
71
100
|
}
|
|
101
|
+
for (const line of formatDatabaseBilling(summary.databases))
|
|
102
|
+
console.log(line);
|
|
72
103
|
}
|
|
73
104
|
catch (err) {
|
|
74
105
|
fail(err);
|
package/dist/commands/db-dump.js
CHANGED
|
@@ -8,8 +8,8 @@ import { dumpRemoteSchema, openRemoteAppConnection } from "../lib/db/remote-engi
|
|
|
8
8
|
import { isPlanGateError } from "../utils/plan-gate-error.js";
|
|
9
9
|
import { defaultRemoteDeps, describeRemoteError, remoteEngineDeps, resolveAppId, } from "./db-remote-deps.js";
|
|
10
10
|
export function describeSessionGap(error) {
|
|
11
|
-
const status = error
|
|
12
|
-
if (status === 404 || status === 405) {
|
|
11
|
+
const { status, code } = error;
|
|
12
|
+
if ((status === 404 || status === 405) && !code) {
|
|
13
13
|
return "Remote database access for apps is not available yet. The HQ API has no database session route.";
|
|
14
14
|
}
|
|
15
15
|
return null;
|
|
@@ -71,6 +71,12 @@ export function describeRemoteError(error) {
|
|
|
71
71
|
if (status === 404 && code === "DB_USAGE_NOT_PROVISIONED") {
|
|
72
72
|
return "This company has no remote database yet. Run hq db provision first.";
|
|
73
73
|
}
|
|
74
|
+
if (status === 404 && code === "DB_APP_REMOVED") {
|
|
75
|
+
return "This app's database was removed, so it can't be opened. Run hq db usage --app <app> to see how long its data is kept.";
|
|
76
|
+
}
|
|
77
|
+
if (status === 404 && code === "DB_APP_NOT_FOUND") {
|
|
78
|
+
return "This app has no database in this company. Check the app name, or deploy it with database enabled.";
|
|
79
|
+
}
|
|
74
80
|
if (status === 404 && code === "DB_USAGE_APP_NOT_FOUND") {
|
|
75
81
|
return "No database found for that app in this company. Check the app name, or deploy it with database enabled.";
|
|
76
82
|
}
|
|
@@ -176,12 +176,13 @@ export async function runLaneLoopController(ctx, laneId, deps) {
|
|
|
176
176
|
signal.removeEventListener("abort", abortPhase);
|
|
177
177
|
}
|
|
178
178
|
},
|
|
179
|
-
raiseQuestion: ({ envelope, envelopeId, attempt, error }) => {
|
|
179
|
+
raiseQuestion: ({ envelope, envelopeId, attempt, error, questionId }) => {
|
|
180
180
|
const current = readLane(hqRoot, laneId);
|
|
181
181
|
const data = envelope;
|
|
182
182
|
raiseLaneQuestion({
|
|
183
183
|
hqRoot,
|
|
184
184
|
lane: current,
|
|
185
|
+
questionId,
|
|
185
186
|
raisedVia: "cli",
|
|
186
187
|
at: ctx.now().toISOString(),
|
|
187
188
|
body: {
|
|
@@ -189,6 +190,7 @@ export async function runLaneLoopController(ctx, laneId, deps) {
|
|
|
189
190
|
context: `${error}\nThe phase is parked; later envelopes remain queued. Stall attempt: ${attempt}.`,
|
|
190
191
|
options: ["Retry with a longer deadline", "Reroute to another worker", "Drop this phase and block the story"],
|
|
191
192
|
recommended: "Retry with a longer deadline",
|
|
193
|
+
loop_stall: { lane_id: laneId, envelope_id: envelopeId, attempt },
|
|
192
194
|
},
|
|
193
195
|
});
|
|
194
196
|
},
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
1
2
|
import { buildOutpostQuestionAnswerCommand, buildOutpostQuestionCancelCommand, buildOutpostQuestionShowCommand, dispatchErrorMessage } from "../lib/lanes/outpost-dispatch.js";
|
|
2
3
|
import { assertNoSecretText } from "../lib/lanes/text-scan.js";
|
|
3
4
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
4
5
|
import { appendCappedHistory, createQuestion, isLanesError, listSessionLiveness, parseLivenessState, parseQuestionRecord, parseQuestionStatus, readDaemonPresenceConnected, readLane, readQuestion, wakeAnsweredLaneNow, sessionLiveness, appendDropboxMessage, answerDropboxMessage, laneAcceptsDropboxDelivery, laneIdForAnswerDelivery, writeQuestionBody } from "../lib/lanes/index.js";
|
|
5
6
|
import { resolveQuestionCompanyWithFallback } from "../lib/lanes/questions.js";
|
|
6
7
|
import { LanesError } from "../lib/lanes/errors.js";
|
|
8
|
+
import { applyLoopStallAnswer } from "../lib/lanes/loop.js";
|
|
7
9
|
export function resolveAnswerChoice(choice, options, freeform) {
|
|
8
10
|
const trimmed = choice.trim();
|
|
9
11
|
if (/^[0-9]+$/.test(trimmed)) {
|
|
@@ -854,6 +856,20 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
854
856
|
},
|
|
855
857
|
};
|
|
856
858
|
});
|
|
859
|
+
if (body.loop_stall?.lane_id === askingLane.lane_id && askingLane.loop_mode === true) {
|
|
860
|
+
const resolved = applyLoopStallAnswer({
|
|
861
|
+
runDir: path.join(hqRoot, "workspace", "lanes-runs", askingLane.lane_id),
|
|
862
|
+
questionId,
|
|
863
|
+
choice,
|
|
864
|
+
...(opts.notes ? { notes: opts.notes } : {}),
|
|
865
|
+
at,
|
|
866
|
+
});
|
|
867
|
+
if (!resolved)
|
|
868
|
+
throw new LanesError("loop_stall_result_missing", `Loop stall result for question ${questionId} is missing`, { question_id: questionId, lane_id: askingLane.lane_id });
|
|
869
|
+
const controllerPid = await deps.restartLoopController(ctx, askingLane.lane_id);
|
|
870
|
+
printJson({ ok: true, action: "questions.answer", lane_id: askingLane.lane_id, question: updated, loop_action: resolved.action, controller_pid: controllerPid });
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
857
873
|
const latestAskingLane = readLane(hqRoot, askingLane.lane_id);
|
|
858
874
|
if (latestAskingLane.state === "awaiting_input") {
|
|
859
875
|
let wake;
|
|
@@ -16,6 +16,18 @@ function enabledFromSettings(root) {
|
|
|
16
16
|
const value = /^\s+default_enabled:\s*["']?(true|false|yes|no|on|off|1|0)["']?\s*(?:#.*)?$/mi.exec(block)?.[1]?.toLowerCase();
|
|
17
17
|
return value === "true" || value === "yes" || value === "on" || value === "1";
|
|
18
18
|
}
|
|
19
|
+
function isUnattended(env) {
|
|
20
|
+
const flags = `${env.HQ_UNATTENDED ?? ""}${env.HQ_SESSION_UNATTENDED ?? ""}${env.CLAUDE_HEADLESS ?? ""}`;
|
|
21
|
+
if (["1", "true", "TRUE", "yes", "YES", "on", "ON"].some((marker) => flags.includes(marker)))
|
|
22
|
+
return true;
|
|
23
|
+
return [
|
|
24
|
+
env.HQ_BOT_AGENT_UID,
|
|
25
|
+
env.HQ_MACHINE_CREDS_FILE,
|
|
26
|
+
env.HQ_AGENT_CLAUDE_TASKFILE,
|
|
27
|
+
env.HQ_AGENT_COMPANY_DIR,
|
|
28
|
+
env.HQ_AGENT_STATUS_FILE,
|
|
29
|
+
].some((marker) => Boolean(marker));
|
|
30
|
+
}
|
|
19
31
|
function sessionWasDisabled(root, env, payloadSessionId) {
|
|
20
32
|
const sessionId = payloadSessionId || env.HQ_SESSION_ID;
|
|
21
33
|
if (!sessionId || !/^[A-Za-z0-9_-]+$/.test(sessionId))
|
|
@@ -169,11 +181,15 @@ export async function runLaneSessionStart(options = {}) {
|
|
|
169
181
|
(options.startSweep ?? ((r, e) => startSessionStartOrphanSweep(r, e, spawn)))(root, env);
|
|
170
182
|
if (sessionWasDisabled(root, env, payloadSessionId))
|
|
171
183
|
return;
|
|
184
|
+
const autoConduct = env.HQ_AUTO_CONDUCT ?? "";
|
|
185
|
+
const forceOn = /^(1|true|on|yes)$/i.test(autoConduct);
|
|
186
|
+
if (/^(0|false|off|no)$/i.test(autoConduct))
|
|
187
|
+
return;
|
|
188
|
+
if (!forceOn && isUnattended(env))
|
|
189
|
+
return;
|
|
172
190
|
let enabled = enabledFromSettings(root);
|
|
173
|
-
if (
|
|
191
|
+
if (forceOn)
|
|
174
192
|
enabled = true;
|
|
175
|
-
else if (/^(0|false|off|no)$/i.test(env.HQ_AUTO_CONDUCT ?? ""))
|
|
176
|
-
enabled = false;
|
|
177
193
|
if (!enabled)
|
|
178
194
|
return;
|
|
179
195
|
const core = path.join(root, ".claude", "skills", "conduct", "conductor-core.md");
|
|
@@ -474,6 +474,7 @@ export interface LaneQuestionsHandlerDeps {
|
|
|
474
474
|
printJson(value: unknown): void;
|
|
475
475
|
readSessionMeta(hqRoot: string, sessionId: string, ctx: Ctx): Record<string, unknown> | null;
|
|
476
476
|
reconcileHostFromCtx(ctx: Ctx): ReconcileHost;
|
|
477
|
+
restartLoopController(ctx: Ctx, laneId: string): Promise<number>;
|
|
477
478
|
resolveActiveQuestionCompany(ctx: Ctx, message?: string): Promise<LaneCompanyRef>;
|
|
478
479
|
resolveAudience(hqRoot: string, question: QuestionRecord, ctx: Ctx): SeniorRef;
|
|
479
480
|
sameQuestionAnswer(left: QuestionAnswer | undefined, right: QuestionAnswer): boolean;
|
package/dist/commands/lanes.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
import type { MonitorLaneMessagePayload, MonitorLaneMessageResult } from "./lanes-message.js";
|
|
10
|
-
import { type LanesMesh, type OutpostRouteIdentity, type SpawnLoginResult } from "./lanes-shared.js";
|
|
10
|
+
import { type Ctx, type LanesMesh, type OutpostRouteIdentity, type SpawnLoginResult } from "./lanes-shared.js";
|
|
11
11
|
export type { LanesMesh, SpawnLoginResult } from "./lanes-shared.js";
|
|
12
12
|
export { CLAUDE_ACCOUNT_BALANCE_SIGNALS_FLAG, LANE_OUTPOST_LOAD_BALANCING_FLAG } from "./lanes-shared.js";
|
|
13
13
|
export { resolveAnswerChoice } from "./lanes-questions.js";
|
|
@@ -36,6 +36,8 @@ export interface LanesCommandDeps {
|
|
|
36
36
|
onLaneWorkerStarted?: (laneId: string, command: "message" | "resume") => void;
|
|
37
37
|
/** Test seam for failures after a message is durably queued. */
|
|
38
38
|
onLaneMessageQueued?: (laneId: string, path: "admission" | "account_move" | "quota_hold") => void;
|
|
39
|
+
/** Test seam for routing loop stall answers back through their controller. */
|
|
40
|
+
restartLoopController?: (ctx: Ctx, laneId: string) => Promise<number>;
|
|
39
41
|
hqRoot?: () => string;
|
|
40
42
|
/** Start HQ-root walk here instead of process.cwd(). Tests only. */
|
|
41
43
|
cwd?: () => string;
|
package/dist/commands/lanes.js
CHANGED
|
@@ -43,7 +43,7 @@ import { appendProjectFocusOverride, readProjectStoryFocusData, removeProjectFoc
|
|
|
43
43
|
import { appendLaneFocusOverride, assertFocusGuardAllows, makeFocusOverride } from "../lib/lanes/focus-guard.js";
|
|
44
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, LANE_CONDUCT_LOOP_FLAG, } from "../lib/lanes/flag-keys.js";
|
|
45
45
|
import { findWorkerContinuityLane, sameLaneHost, workerContinuityPrompt } from "../lib/lanes/worker-continuity.js";
|
|
46
|
-
import { enqueueLoopEnvelope, interruptLoopQueue, loopControllerIsAlive, loopStatus, validateLoopEnvelope } from "../lib/lanes/loop.js";
|
|
46
|
+
import { enqueueLoopEnvelope, interruptLoopQueue, interruptLoopQueueForStory, loopControllerIsAlive, loopStatus, validateLoopEnvelope } from "../lib/lanes/loop.js";
|
|
47
47
|
import { runLaneLoopController, startLaneLoopController } from "./lanes-loop.js";
|
|
48
48
|
import { configuredCodexRolloutHomes, retainCodexRollouts } from "../lib/lanes/disk-guard.js";
|
|
49
49
|
import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
|
|
@@ -580,6 +580,13 @@ export function parseQuestionBody(raw) {
|
|
|
580
580
|
return null;
|
|
581
581
|
if (obj.notes !== undefined && typeof obj.notes !== "string")
|
|
582
582
|
return null;
|
|
583
|
+
if (obj.loop_stall !== undefined && (typeof obj.loop_stall !== "object" || obj.loop_stall === null || Array.isArray(obj.loop_stall) ||
|
|
584
|
+
typeof obj.loop_stall.lane_id !== "string" ||
|
|
585
|
+
typeof obj.loop_stall.envelope_id !== "string" ||
|
|
586
|
+
typeof obj.loop_stall.attempt !== "number" ||
|
|
587
|
+
!Number.isSafeInteger(obj.loop_stall.attempt) ||
|
|
588
|
+
obj.loop_stall.attempt < 1))
|
|
589
|
+
return null;
|
|
583
590
|
const options = obj.options.filter((item) => typeof item === "string");
|
|
584
591
|
const body = {
|
|
585
592
|
question: obj.question,
|
|
@@ -590,6 +597,8 @@ export function parseQuestionBody(raw) {
|
|
|
590
597
|
body.recommended = obj.recommended;
|
|
591
598
|
if (typeof obj.notes === "string")
|
|
592
599
|
body.notes = obj.notes;
|
|
600
|
+
if (obj.loop_stall !== undefined)
|
|
601
|
+
body.loop_stall = obj.loop_stall;
|
|
593
602
|
return body;
|
|
594
603
|
}
|
|
595
604
|
function readQuestionBodyFile(hqRoot, question) {
|
|
@@ -8614,6 +8623,29 @@ async function runInterrupt(ctx, laneId, reason, options = {}) {
|
|
|
8614
8623
|
throw new LanesError("interrupt_remote_unsupported", `Lane ${laneId} runs on Outpost ${lane.host.id}; run hq lanes interrupt on that Outpost`, { lane_id: laneId, outpost_id: lane.host.id });
|
|
8615
8624
|
}
|
|
8616
8625
|
assertLaneRunsHereForOperatorMutation(ctx, lane, "interrupt");
|
|
8626
|
+
if (options.story !== undefined && lane.loop_mode !== true) {
|
|
8627
|
+
throw new LanesError("usage_error", "--story is only supported for loop lanes", { lane_id: laneId });
|
|
8628
|
+
}
|
|
8629
|
+
if (options.phase !== undefined && options.story === undefined) {
|
|
8630
|
+
throw new LanesError("usage_error", "--phase requires --story", { lane_id: laneId });
|
|
8631
|
+
}
|
|
8632
|
+
if (lane.loop_mode === true && options.story !== undefined) {
|
|
8633
|
+
const scoped = interruptLoopQueueForStory(runDir(hqRoot, lane.lane_id), options.story, options.phase);
|
|
8634
|
+
if (scoped.already_picked_up.length === 0 || lane.state !== "running") {
|
|
8635
|
+
if (!options.quiet)
|
|
8636
|
+
printJson({ ok: true, action: "interrupt", lane_id: lane.lane_id, terminal: false, resumable: true, ...scoped });
|
|
8637
|
+
return;
|
|
8638
|
+
}
|
|
8639
|
+
options = { ...options, preserveLoopQueue: true };
|
|
8640
|
+
// The matching active phase uses the same provider interruption path as a deadline abort.
|
|
8641
|
+
if (!reason)
|
|
8642
|
+
reason = "story-scoped loop phase interrupt";
|
|
8643
|
+
const remaining = scoped;
|
|
8644
|
+
await runInterrupt(ctx, laneId, reason, { preserveLoopQueue: true, quiet: true });
|
|
8645
|
+
if (!options.quiet)
|
|
8646
|
+
printJson({ ok: true, action: "interrupt", lane_id: lane.lane_id, terminal: false, resumable: true, ...remaining });
|
|
8647
|
+
return;
|
|
8648
|
+
}
|
|
8617
8649
|
const queueInterrupt = lane.loop_mode === true && options.preserveLoopQueue !== true
|
|
8618
8650
|
? interruptLoopQueue(runDir(hqRoot, lane.lane_id))
|
|
8619
8651
|
: undefined;
|
|
@@ -9390,6 +9422,22 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9390
9422
|
printJson,
|
|
9391
9423
|
readSessionMeta,
|
|
9392
9424
|
reconcileHostFromCtx,
|
|
9425
|
+
restartLoopController: async (handlerCtx, laneId) => {
|
|
9426
|
+
if (deps.restartLoopController)
|
|
9427
|
+
return deps.restartLoopController(handlerCtx, laneId);
|
|
9428
|
+
const current = readLane(handlerCtx.hqRoot(), laneId);
|
|
9429
|
+
if (current.state === "running") {
|
|
9430
|
+
await runInterrupt(handlerCtx, laneId, "loop stall question answered; restarting controller", { preserveLoopQueue: true, quiet: true });
|
|
9431
|
+
}
|
|
9432
|
+
const runDirPath = runDir(handlerCtx.hqRoot(), laneId);
|
|
9433
|
+
if (loopControllerIsAlive(runDirPath)) {
|
|
9434
|
+
const pid = loopStatus(runDirPath, laneId).pid;
|
|
9435
|
+
if (pid === undefined)
|
|
9436
|
+
throw new LanesError("loop_controller_pid_missing", `Loop controller for ${laneId} is alive without a recorded pid`, { lane_id: laneId });
|
|
9437
|
+
return pid;
|
|
9438
|
+
}
|
|
9439
|
+
return startLaneLoopController(handlerCtx, laneId);
|
|
9440
|
+
},
|
|
9393
9441
|
resolveActiveQuestionCompany,
|
|
9394
9442
|
resolveAudience,
|
|
9395
9443
|
sameQuestionAnswer,
|
|
@@ -10034,8 +10082,10 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
10034
10082
|
.description("Stop a running lane's turn and keep it resumable with hq lanes resume")
|
|
10035
10083
|
.argument("<lane-id>", "Lane id")
|
|
10036
10084
|
.option("--reason <text>", "Record the reason in the lane's stop history")
|
|
10085
|
+
.option("--story <id>", "Withdraw only this story's queued phases (loop lanes only)")
|
|
10086
|
+
.option("--phase <name>", "Limit --story interruption to one phase")
|
|
10037
10087
|
.action(wrap(async (laneId, options) => {
|
|
10038
|
-
await runInterrupt(ctx, laneId, options.reason);
|
|
10088
|
+
await runInterrupt(ctx, laneId, options.reason, options);
|
|
10039
10089
|
}));
|
|
10040
10090
|
lanes
|
|
10041
10091
|
.command("approve")
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type FlagClient, type FlagClientOptions } from "@indigoai-us/hq-flags-client";
|
|
2
|
+
export declare const AGENT_KIT_INBOX_BACKOFF_FLAG = "agents.inbox-poll-backoff-agent-kit";
|
|
3
|
+
export declare const AGENT_KIT_INBOX_BACKOFF_DEFAULT = true;
|
|
4
|
+
export declare const AGENT_KIT_INBOX_BACKOFF_CACHE_MS: number;
|
|
5
|
+
export declare const AGENT_KIT_INBOX_BACKOFF_TIMEOUT_MS = 2000;
|
|
6
|
+
type InboxBackoffFlagClient = Pick<FlagClient, "ready" | "isEnabled" | "close">;
|
|
7
|
+
export interface AgentKitInboxBackoffFlagDependencies {
|
|
8
|
+
companySlug?: string;
|
|
9
|
+
getToken: FlagClientOptions["getToken"];
|
|
10
|
+
createClient?: (options: FlagClientOptions) => InboxBackoffFlagClient;
|
|
11
|
+
endpoint?: string;
|
|
12
|
+
fetch?: typeof fetch;
|
|
13
|
+
now?: () => number;
|
|
14
|
+
reportFailure?: (error: unknown) => void;
|
|
15
|
+
}
|
|
16
|
+
/** Read the default-on kill switch; registry outages preserve idle backoff. */
|
|
17
|
+
export declare function createAgentKitInboxBackoffFlagReader(dependencies: AgentKitInboxBackoffFlagDependencies): () => Promise<boolean>;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=inbox-poll-backoff-flag.d.ts.map
|