@indigoai-us/hq-cli 5.345.76 → 5.345.78
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 +38 -0
- package/dist/command-catalog.generated.d.ts +83 -3
- package/dist/command-catalog.generated.js +100 -3
- package/dist/commands/access.d.ts +1 -0
- package/dist/commands/access.js +17 -5
- package/dist/commands/bot.d.ts +12 -0
- package/dist/commands/bot.js +54 -2
- package/dist/commands/files.js +7 -2
- package/dist/commands/members.d.ts +42 -0
- package/dist/commands/members.js +151 -1
- package/dist/commands/secrets.js +1 -1
- package/dist/lib/bot/api.d.ts +11 -0
- package/dist/lib/bot/api.js +2 -0
- package/dist/lib/bot/inflight.d.ts +3 -0
- package/dist/lib/bot/probe.d.ts +57 -0
- package/dist/lib/bot/probe.js +100 -0
- package/dist/lib/bot/run.d.ts +4 -0
- package/dist/lib/bot/run.js +171 -37
- package/dist/lib/bot/runtime/codex.d.ts +45 -1
- package/dist/lib/bot/runtime/codex.js +134 -2
- package/dist/lib/bot/runtime/grok.js +2 -2
- package/dist/lib/bot/runtime/index.d.ts +8 -0
- package/dist/lib/bot/runtime/index.js +91 -5
- package/dist/lib/bot/runtime/messages-stream.js +1 -1
- package/dist/lib/bot/runtime/types.d.ts +33 -0
- package/dist/lib/bot/runtime/types.js +10 -0
- package/dist/lib/bot/runtime-failure.d.ts +113 -0
- package/dist/lib/bot/runtime-failure.js +180 -0
- package/dist/lib/bot/runtime-sign-in.js +2 -2
- package/dist/lib/bot/status.d.ts +21 -0
- package/dist/lib/lanes/spawn.js +1 -0
- package/dist/lib/lanes/store.js +41 -25
- package/package.json +1 -1
package/dist/commands/members.js
CHANGED
|
@@ -371,6 +371,108 @@ export async function changeMemberRole(token, companyUid, membershipKey, newRole
|
|
|
371
371
|
throw new InviteHttpError(res.status, err.message ?? err.error ?? res.statusText, err.code);
|
|
372
372
|
}
|
|
373
373
|
}
|
|
374
|
+
/** Guests remain active members, so company-wide (@all) grants still apply. */
|
|
375
|
+
export const GUEST_COMPANY_WIDE_NOTE = "Guests stay active members. Company-wide grants (Everyone in company / @all) still apply to them. Demoting to guest does not lock someone out. Use `hq members remove <email>`.";
|
|
376
|
+
export function personUidFromMembershipKey(membershipKey) {
|
|
377
|
+
const hash = membershipKey.indexOf("#");
|
|
378
|
+
return hash === -1 ? membershipKey : membershipKey.slice(0, hash);
|
|
379
|
+
}
|
|
380
|
+
/** Paths granted to this person or their email — not company-wide or group. */
|
|
381
|
+
export function personalGrantPaths(grants) {
|
|
382
|
+
if (!grants)
|
|
383
|
+
return [];
|
|
384
|
+
const paths = [];
|
|
385
|
+
for (const grant of grants) {
|
|
386
|
+
if (typeof grant.path !== "string" || grant.path.length === 0)
|
|
387
|
+
continue;
|
|
388
|
+
const sources = Array.isArray(grant.sources) ? grant.sources : [];
|
|
389
|
+
if (sources.some((source) => source.via === "person" || source.via === "email")) {
|
|
390
|
+
paths.push(grant.path);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return [...new Set(paths)];
|
|
394
|
+
}
|
|
395
|
+
export async function fetchMemberAccessProjection(token, companyUid, personUid) {
|
|
396
|
+
const res = await vaultApiFetch({
|
|
397
|
+
token,
|
|
398
|
+
path: `/files/${encodeURIComponent(companyUid)}/members/${encodeURIComponent(personUid)}/access`,
|
|
399
|
+
method: "GET",
|
|
400
|
+
});
|
|
401
|
+
if (!res.ok)
|
|
402
|
+
return null;
|
|
403
|
+
return (await res.json().catch(() => null));
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* End an active membership via `POST /membership/revoke`, then drop person and
|
|
407
|
+
* email file/secret grants collected from the member-access projection.
|
|
408
|
+
* Company-wide and group grants are left in place — they belong to the company,
|
|
409
|
+
* not the person. Membership end already makes company-wide grants inert for
|
|
410
|
+
* this principal because they are no longer an active member.
|
|
411
|
+
*/
|
|
412
|
+
export async function removeActiveMember(token, companyUid, membershipKey, opts) {
|
|
413
|
+
const personUid = personUidFromMembershipKey(membershipKey);
|
|
414
|
+
const projection = await fetchMemberAccessProjection(token, companyUid, personUid);
|
|
415
|
+
const email = opts?.email ??
|
|
416
|
+
(typeof projection?.identity?.primaryEmail === "string"
|
|
417
|
+
? projection.identity.primaryEmail
|
|
418
|
+
: undefined);
|
|
419
|
+
const filePaths = personalGrantPaths(projection?.files?.grants);
|
|
420
|
+
const secretPaths = personalGrantPaths(projection?.secrets?.grants);
|
|
421
|
+
await revokeInvite(token, membershipKey, companyUid);
|
|
422
|
+
const droppedFileGrants = [];
|
|
423
|
+
const droppedSecretGrants = [];
|
|
424
|
+
const skippedGrantDrops = [];
|
|
425
|
+
for (const prefix of filePaths) {
|
|
426
|
+
const personRes = await vaultApiFetch({
|
|
427
|
+
token,
|
|
428
|
+
path: `/files/${encodeURIComponent(companyUid)}/acl/revoke`,
|
|
429
|
+
method: "POST",
|
|
430
|
+
body: { prefix, granteeType: "person", granteeId: personUid },
|
|
431
|
+
});
|
|
432
|
+
let dropped = personRes.ok;
|
|
433
|
+
if (!dropped && email) {
|
|
434
|
+
const emailRes = await vaultApiFetch({
|
|
435
|
+
token,
|
|
436
|
+
path: `/files/${encodeURIComponent(companyUid)}/acl/revoke`,
|
|
437
|
+
method: "POST",
|
|
438
|
+
body: {
|
|
439
|
+
prefix,
|
|
440
|
+
granteeType: "email",
|
|
441
|
+
granteeId: email.toLowerCase(),
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
dropped = emailRes.ok;
|
|
445
|
+
}
|
|
446
|
+
if (dropped)
|
|
447
|
+
droppedFileGrants.push(prefix);
|
|
448
|
+
else
|
|
449
|
+
skippedGrantDrops.push(prefix);
|
|
450
|
+
}
|
|
451
|
+
if (email) {
|
|
452
|
+
for (const secretPath of secretPaths) {
|
|
453
|
+
const res = await vaultApiFetch({
|
|
454
|
+
token,
|
|
455
|
+
path: `/secrets/${encodeURIComponent(companyUid)}/acl/revoke`,
|
|
456
|
+
method: "POST",
|
|
457
|
+
body: {
|
|
458
|
+
path: secretPath,
|
|
459
|
+
granteeType: "email",
|
|
460
|
+
granteeId: email.toLowerCase(),
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
if (res.ok)
|
|
464
|
+
droppedSecretGrants.push(secretPath);
|
|
465
|
+
else
|
|
466
|
+
skippedGrantDrops.push(secretPath);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
membershipKey,
|
|
471
|
+
droppedFileGrants,
|
|
472
|
+
droppedSecretGrants,
|
|
473
|
+
skippedGrantDrops,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
374
476
|
export function registerMembersCommand(program) {
|
|
375
477
|
const members = program
|
|
376
478
|
.command("members")
|
|
@@ -384,6 +486,7 @@ export function registerMembersCommand(program) {
|
|
|
384
486
|
promote: "members:write",
|
|
385
487
|
invite: "members:write",
|
|
386
488
|
revoke: "members:write",
|
|
489
|
+
remove: "members:write",
|
|
387
490
|
},
|
|
388
491
|
});
|
|
389
492
|
// Canonical role-change command. `promote` is the discoverable verb users
|
|
@@ -418,6 +521,10 @@ export function registerMembersCommand(program) {
|
|
|
418
521
|
console.log(chalk.dim(`${role === "owner" ? "Owners" : "Admins"} automatically have full read and write access to all of this company's context — no separate context grant is needed.\n` +
|
|
419
522
|
`What they actually see synced to their machine is governed by their sync mode (\`hq sync mode\`), not by access.`));
|
|
420
523
|
}
|
|
524
|
+
else if (role === "guest") {
|
|
525
|
+
console.log(chalk.dim(GUEST_COMPANY_WIDE_NOTE));
|
|
526
|
+
console.log(chalk.dim(`Check what they can still reach with: hq access <path> --as ${target}`));
|
|
527
|
+
}
|
|
421
528
|
}
|
|
422
529
|
catch (err) {
|
|
423
530
|
if (err instanceof InviteHttpError) {
|
|
@@ -708,9 +815,52 @@ export function registerMembersCommand(program) {
|
|
|
708
815
|
process.exit(1);
|
|
709
816
|
}
|
|
710
817
|
});
|
|
818
|
+
members
|
|
819
|
+
.command("remove <target>")
|
|
820
|
+
.description("Remove an active member. Ends membership so company-wide grants no longer apply, and drops that person's file and secret grants. Accepts an email, prs_ personUid, agt_ agentUid, or full membership key.")
|
|
821
|
+
.action(async (target) => {
|
|
822
|
+
try {
|
|
823
|
+
const token = (await resolveVaultCredential()).token;
|
|
824
|
+
const companySlug = members.opts().company;
|
|
825
|
+
const companyUid = await getCompanyUid(token, companySlug);
|
|
826
|
+
const detected = detectTarget(target);
|
|
827
|
+
const membershipKey = await resolveRoleChangeTarget(token, companyUid, target);
|
|
828
|
+
const email = detected?.type === "email" ? detected.value : undefined;
|
|
829
|
+
const result = await removeActiveMember(token, companyUid, membershipKey, { email });
|
|
830
|
+
console.log(chalk.green(`Removed '${target}' from the company (membership ${result.membershipKey}).`));
|
|
831
|
+
console.log(chalk.dim("They are no longer an active member, so company-wide grants no longer apply to them."));
|
|
832
|
+
if (result.droppedFileGrants.length > 0) {
|
|
833
|
+
console.log(chalk.dim(`Dropped person file grants: ${result.droppedFileGrants.join(", ")}`));
|
|
834
|
+
}
|
|
835
|
+
if (result.droppedSecretGrants.length > 0) {
|
|
836
|
+
console.log(chalk.dim(`Dropped person secret grants: ${result.droppedSecretGrants.join(", ")}`));
|
|
837
|
+
}
|
|
838
|
+
if (result.skippedGrantDrops.length > 0) {
|
|
839
|
+
console.log(chalk.yellow(`Could not drop some person grants: ${result.skippedGrantDrops.join(", ")}`));
|
|
840
|
+
}
|
|
841
|
+
console.log(chalk.dim("A remaining teammate's access: hq access <path> --as <email>. After removal that check reports they are not an active member."));
|
|
842
|
+
}
|
|
843
|
+
catch (err) {
|
|
844
|
+
if (err instanceof InviteHttpError) {
|
|
845
|
+
const msg = err.status === 403
|
|
846
|
+
? err.code === "ADMIN_ROLE_TARGET_RESTRICTED"
|
|
847
|
+
? "Admin can only remove role=member; ask an owner to remove this membership"
|
|
848
|
+
: "Not authorized — only admins and owners can remove members"
|
|
849
|
+
: err.status === 404
|
|
850
|
+
? "Member not found — they may already have been removed"
|
|
851
|
+
: err.status === 409 && err.code === "LAST_OWNER_UNREMOVABLE"
|
|
852
|
+
? "A company must keep at least one owner"
|
|
853
|
+
: formatInviteHttpError(err.status, err.message, err.code);
|
|
854
|
+
console.error(chalk.red(msg));
|
|
855
|
+
process.exit(1);
|
|
856
|
+
}
|
|
857
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
858
|
+
process.exit(1);
|
|
859
|
+
}
|
|
860
|
+
});
|
|
711
861
|
members
|
|
712
862
|
.command("revoke <target>")
|
|
713
|
-
.description("Revoke a pending invite. Accepts an email, personUid, full membershipKey, or legacy inviteToken.")
|
|
863
|
+
.description("Revoke a pending invite. For an active member, use `hq members remove` instead. Accepts an email, personUid, full membershipKey, or legacy inviteToken.")
|
|
714
864
|
.action(async (target) => {
|
|
715
865
|
try {
|
|
716
866
|
const token = (await resolveVaultCredential()).token;
|
package/dist/commands/secrets.js
CHANGED
|
@@ -385,7 +385,7 @@ function parseSecretAclPrincipal(principal) {
|
|
|
385
385
|
}
|
|
386
386
|
function describeSecretAclPrincipal(principal) {
|
|
387
387
|
return principal.granteeType === "company-wide"
|
|
388
|
-
? "@all (entire company)"
|
|
388
|
+
? "@all (entire company, including guests)"
|
|
389
389
|
: principal.granteeId;
|
|
390
390
|
}
|
|
391
391
|
// Mirrors hq-pro's server-authoritative KNOWN_DESTINATION_REGISTRY
|
package/dist/lib/bot/api.d.ts
CHANGED
|
@@ -217,6 +217,17 @@ export declare class BotApi {
|
|
|
217
217
|
body: string;
|
|
218
218
|
rootEventId?: string;
|
|
219
219
|
audience?: "human" | "agent" | "both";
|
|
220
|
+
/**
|
|
221
|
+
* Structured blocks shown next to the plain body (server: hq-pro-core
|
|
222
|
+
* vault-service/lib/rich-content.ts, `{ v: 1, blocks: [{ kind, ... }] }`).
|
|
223
|
+
* The body stays the fallback for clients that do not render them.
|
|
224
|
+
*/
|
|
225
|
+
richContent?: {
|
|
226
|
+
v: 1;
|
|
227
|
+
blocks: ReadonlyArray<{
|
|
228
|
+
kind: string;
|
|
229
|
+
}>;
|
|
230
|
+
};
|
|
220
231
|
}): Promise<{
|
|
221
232
|
eventId?: string;
|
|
222
233
|
createdAt?: string;
|
package/dist/lib/bot/api.js
CHANGED
|
@@ -158,6 +158,8 @@ export class BotApi {
|
|
|
158
158
|
payload.rootEventId = input.rootEventId;
|
|
159
159
|
if (input.audience)
|
|
160
160
|
payload.audience = input.audience;
|
|
161
|
+
if (input.richContent)
|
|
162
|
+
payload.richContent = input.richContent;
|
|
161
163
|
const { body } = await this.call("/v1/notify/dm", { method: "POST", body: payload });
|
|
162
164
|
return body;
|
|
163
165
|
}
|
|
@@ -66,6 +66,9 @@ export interface InflightTurn {
|
|
|
66
66
|
*/
|
|
67
67
|
jobFailure?: {
|
|
68
68
|
reason: string;
|
|
69
|
+
/** The plain run output to report (never the runtime's raw text). */
|
|
70
|
+
output?: string;
|
|
71
|
+
/** Older markers: the raw excerpt. Not reported anywhere since 5.345.76. */
|
|
69
72
|
excerpt?: string;
|
|
70
73
|
};
|
|
71
74
|
rootEventId?: string;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Readiness probe: one minimal model turn with the exact runtime, model and
|
|
3
|
+
* thinking level a bot would use, so the desktop can tell before saving a bot
|
|
4
|
+
* whether its first turn will work (`hq bot probe --runtime codex --json`).
|
|
5
|
+
*
|
|
6
|
+
* The turn goes through the same `runRuntimeTurn` a bot uses, with the same
|
|
7
|
+
* environment rules as a personal bot (`turnEnv`) and the same approval mode,
|
|
8
|
+
* so a version mismatch, a dead sign-in or an unknown model shows up here the
|
|
9
|
+
* way it would on the bot's first message. The version-mismatch retry applies
|
|
10
|
+
* too: a probe that only worked on a fallback model is `ok` and says so in
|
|
11
|
+
* `modelFallback`. It runs in an empty scratch directory, not the HQ root, so
|
|
12
|
+
* the tool loads no project settings, instructions or hooks: the check costs
|
|
13
|
+
* one tiny model turn and nothing else.
|
|
14
|
+
*/
|
|
15
|
+
import type { BotRuntimeId } from "./config.js";
|
|
16
|
+
import { type RuntimeFailureClass } from "./runtime-failure.js";
|
|
17
|
+
import { type BotRuntime, type RunTurnDeps, type RuntimeTurnInput, type RuntimeTurnResult } from "./runtime/index.js";
|
|
18
|
+
export declare const PROBE_PROMPT = "This is a readiness check from HQ. Reply with exactly the word OK and nothing else. Do not run any commands or use any tools.";
|
|
19
|
+
export declare const PROBE_SYSTEM_PROMPT = "You are an HQ bot being checked before it is created. Answer the check in one word.";
|
|
20
|
+
/** A probe that has not answered by then is reported as `transient`. */
|
|
21
|
+
export declare const PROBE_DEFAULT_TIMEOUT_MS = 120000;
|
|
22
|
+
/** The plain sentence for a failed probe: what is wrong and the one fix. */
|
|
23
|
+
export declare function probeFailureText(cls: RuntimeFailureClass, runtime: BotRuntimeId | string): string;
|
|
24
|
+
export interface RuntimeProbeResult {
|
|
25
|
+
ok: boolean;
|
|
26
|
+
/** null when ok; otherwise the same classes a bot's failure DM uses. */
|
|
27
|
+
class: RuntimeFailureClass | null;
|
|
28
|
+
/** One plain sentence a person can read. Never the runtime's raw error. */
|
|
29
|
+
detail: string;
|
|
30
|
+
runtime: BotRuntimeId;
|
|
31
|
+
/** The model asked for; null = the runtime's own default. */
|
|
32
|
+
model: string | null;
|
|
33
|
+
/** The CLI was too old for `from`, and the probe answered on `to`. */
|
|
34
|
+
modelFallback?: {
|
|
35
|
+
from: string;
|
|
36
|
+
to: string;
|
|
37
|
+
};
|
|
38
|
+
/** The runtime's raw error, for logs only. Not for display. */
|
|
39
|
+
diagnostic?: string;
|
|
40
|
+
durationMs: number;
|
|
41
|
+
}
|
|
42
|
+
export interface ProbeOptions {
|
|
43
|
+
runtime: BotRuntimeId;
|
|
44
|
+
model?: string;
|
|
45
|
+
effort?: string;
|
|
46
|
+
/** Working directory of the turn. Default: an empty scratch directory, removed afterwards. */
|
|
47
|
+
cwd?: string;
|
|
48
|
+
autoApprove?: boolean;
|
|
49
|
+
timeoutMs?: number;
|
|
50
|
+
env?: NodeJS.ProcessEnv;
|
|
51
|
+
/** Test seams. */
|
|
52
|
+
runTurn?: (runtime: BotRuntime, input: RuntimeTurnInput, deps?: RunTurnDeps) => Promise<RuntimeTurnResult>;
|
|
53
|
+
runtimeImpl?: BotRuntime;
|
|
54
|
+
now?: () => number;
|
|
55
|
+
}
|
|
56
|
+
export declare function probeBotRuntime(opts: ProbeOptions): Promise<RuntimeProbeResult>;
|
|
57
|
+
//# sourceMappingURL=probe.d.ts.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Readiness probe: one minimal model turn with the exact runtime, model and
|
|
3
|
+
* thinking level a bot would use, so the desktop can tell before saving a bot
|
|
4
|
+
* whether its first turn will work (`hq bot probe --runtime codex --json`).
|
|
5
|
+
*
|
|
6
|
+
* The turn goes through the same `runRuntimeTurn` a bot uses, with the same
|
|
7
|
+
* environment rules as a personal bot (`turnEnv`) and the same approval mode,
|
|
8
|
+
* so a version mismatch, a dead sign-in or an unknown model shows up here the
|
|
9
|
+
* way it would on the bot's first message. The version-mismatch retry applies
|
|
10
|
+
* too: a probe that only worked on a fallback model is `ok` and says so in
|
|
11
|
+
* `modelFallback`. It runs in an empty scratch directory, not the HQ root, so
|
|
12
|
+
* the tool loads no project settings, instructions or hooks: the check costs
|
|
13
|
+
* one tiny model turn and nothing else.
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from "node:fs";
|
|
16
|
+
import * as os from "node:os";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
import { classifyRuntimeError } from "./runtime-failure.js";
|
|
19
|
+
import { runtimeDisplayName } from "./runtime-sign-in.js";
|
|
20
|
+
import { RuntimeError, runRuntimeTurn, runtimeFor } from "./runtime/index.js";
|
|
21
|
+
import { turnEnv } from "./run.js";
|
|
22
|
+
export const PROBE_PROMPT = "This is a readiness check from HQ. Reply with exactly the word OK and nothing else. Do not run any commands or use any tools.";
|
|
23
|
+
export const PROBE_SYSTEM_PROMPT = "You are an HQ bot being checked before it is created. Answer the check in one word.";
|
|
24
|
+
/** A probe that has not answered by then is reported as `transient`. */
|
|
25
|
+
export const PROBE_DEFAULT_TIMEOUT_MS = 120_000;
|
|
26
|
+
/** The plain sentence for a failed probe: what is wrong and the one fix. */
|
|
27
|
+
export function probeFailureText(cls, runtime) {
|
|
28
|
+
const tool = runtimeDisplayName(runtime);
|
|
29
|
+
switch (cls) {
|
|
30
|
+
case "signed-out":
|
|
31
|
+
return `${tool} is not signed in on this computer. Sign in to ${tool} in HQ, then check again.`;
|
|
32
|
+
case "cli-outdated":
|
|
33
|
+
return `${tool} on this computer is out of date for this model. Update ${tool}, then check again.`;
|
|
34
|
+
case "model-unsupported":
|
|
35
|
+
return `This model isn't available in ${tool} here. Choose a different model, then check again.`;
|
|
36
|
+
case "transient":
|
|
37
|
+
return `${tool} is busy or out of reach right now. Check again in a few minutes.`;
|
|
38
|
+
case "not-installed":
|
|
39
|
+
return `${tool} isn't installed on this computer. Install ${tool} from HQ, then check again.`;
|
|
40
|
+
default:
|
|
41
|
+
return `${tool} stopped with an error. Check again in a minute.`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export async function probeBotRuntime(opts) {
|
|
45
|
+
const now = opts.now ?? Date.now;
|
|
46
|
+
const started = now();
|
|
47
|
+
const runtime = opts.runtimeImpl ?? runtimeFor(opts.runtime);
|
|
48
|
+
const runTurn = opts.runTurn ?? runRuntimeTurn;
|
|
49
|
+
const tool = runtimeDisplayName(opts.runtime);
|
|
50
|
+
const model = opts.model?.trim() || null;
|
|
51
|
+
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), "hq-bot-probe-"));
|
|
52
|
+
const systemPromptFile = path.join(scratch, "system-prompt.md");
|
|
53
|
+
fs.writeFileSync(systemPromptFile, PROBE_SYSTEM_PROMPT + "\n");
|
|
54
|
+
const cwd = opts.cwd ?? path.join(scratch, "work");
|
|
55
|
+
fs.mkdirSync(cwd, { recursive: true });
|
|
56
|
+
const base = { runtime: opts.runtime, model };
|
|
57
|
+
try {
|
|
58
|
+
const turn = await runTurn(runtime, {
|
|
59
|
+
prompt: PROBE_PROMPT,
|
|
60
|
+
systemPrompt: PROBE_SYSTEM_PROMPT,
|
|
61
|
+
systemPromptFile,
|
|
62
|
+
cwd,
|
|
63
|
+
hqRoot: cwd,
|
|
64
|
+
autoApprove: opts.autoApprove !== false,
|
|
65
|
+
...(model ? { model } : {}),
|
|
66
|
+
...(opts.effort?.trim() ? { effort: opts.effort.trim() } : {}),
|
|
67
|
+
env: turnEnv(opts.env ?? process.env, "personal"),
|
|
68
|
+
}, { timeoutMs: opts.timeoutMs ?? PROBE_DEFAULT_TIMEOUT_MS });
|
|
69
|
+
const durationMs = now() - started;
|
|
70
|
+
if (turn.modelFallback) {
|
|
71
|
+
const { from, to } = turn.modelFallback;
|
|
72
|
+
return {
|
|
73
|
+
ok: true,
|
|
74
|
+
class: null,
|
|
75
|
+
detail: `${tool} works, but it is too old for ${from || "its configured model"}, so it answered on ${to}. Update ${tool} to use ${from || "that model"}.`,
|
|
76
|
+
...base,
|
|
77
|
+
modelFallback: turn.modelFallback,
|
|
78
|
+
durationMs,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return { ok: true, class: null, detail: `${tool} answered${model ? ` on ${model}` : ""}.`, ...base, durationMs };
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
const raw = err instanceof RuntimeError ? `${err.message}\n${err.stderrExcerpt}` : err instanceof Error ? err.message : String(err);
|
|
85
|
+
// Same rule as a bot turn: the runtime's reported error decides, never a tail of its output.
|
|
86
|
+
const cls = classifyRuntimeError(err);
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
class: cls,
|
|
90
|
+
detail: probeFailureText(cls, opts.runtime),
|
|
91
|
+
...base,
|
|
92
|
+
diagnostic: raw.slice(0, 400),
|
|
93
|
+
durationMs: now() - started,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
fs.rmSync(scratch, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=probe.js.map
|
package/dist/lib/bot/run.d.ts
CHANGED
|
@@ -147,6 +147,8 @@ export interface BotRunDeps {
|
|
|
147
147
|
maxParallelTurns?: number;
|
|
148
148
|
/** Wait between retries while the coding tool needs a sign-in (default SIGN_IN_RETRY_MS). */
|
|
149
149
|
signInRetryMs?: number;
|
|
150
|
+
/** How long model turns are held after FAILURE_LIMIT failures in a window (default FAILURE_WINDOW_MS). */
|
|
151
|
+
modelHoldMs?: number;
|
|
150
152
|
/**
|
|
151
153
|
* Downloads a chat attachment's bytes with the bot's own machine identity
|
|
152
154
|
* (vault presign). Unset = attachments are listed with a "cannot download"
|
|
@@ -202,6 +204,8 @@ export declare function didNotFinishText(reason: string, progressPosted: boolean
|
|
|
202
204
|
export { containsNoReplySentinel };
|
|
203
205
|
/** Grok emits is_error with this message when the model produces only reasoning tokens. */
|
|
204
206
|
export declare function isReasoningOnlyError(excerpt: string): boolean;
|
|
207
|
+
/** The one note a person gets when the bot pauses after repeated failures. */
|
|
208
|
+
export declare function holdNoticeText(holdMs: number, messagePending: boolean): string;
|
|
205
209
|
/** Owner stop, `hq bot stop` or `hq bot restart`: all arrive as the same signal. */
|
|
206
210
|
export declare const STOPPED_BY_OWNER_REASON = "I was stopped before I was done";
|
|
207
211
|
export declare const RESTARTED_REASON = "I was restarted while I was working on it";
|