@indigoai-us/hq-cli 5.345.198 → 5.345.199
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 +45 -0
- package/dist/command-catalog.generated.d.ts +106 -6
- package/dist/command-catalog.generated.js +130 -6
- package/dist/commands/bot-events.d.ts +19 -0
- package/dist/commands/bot-events.js +46 -0
- package/dist/commands/bot-jobs.d.ts +2 -0
- package/dist/commands/bot-jobs.js +43 -2
- package/dist/commands/bot.d.ts +5 -0
- package/dist/commands/bot.js +33 -1
- package/dist/commands/lanes-report.d.ts +2 -1
- package/dist/commands/lanes-report.js +44 -1
- package/dist/commands/lanes.d.ts +9 -0
- package/dist/commands/lanes.js +95 -9
- package/dist/commands/meetings.js +1 -21
- package/dist/commands/signals-run.d.ts +2 -0
- package/dist/commands/signals-run.js +6 -1
- package/dist/lanes-drain-hook.js +9 -0
- package/dist/lib/bot/api.d.ts +5 -0
- package/dist/lib/bot/claude-login.d.ts +31 -0
- package/dist/lib/bot/claude-login.js +160 -0
- package/dist/lib/bot/events.d.ts +18 -0
- package/dist/lib/bot/events.js +160 -0
- package/dist/lib/bot/executive-settings.d.ts +10 -0
- package/dist/lib/bot/executive-settings.js +43 -0
- package/dist/lib/bot/fleet-cron.js +4 -0
- package/dist/lib/bot/fleet-local.js +10 -10
- package/dist/lib/bot/job-run.js +1 -1
- package/dist/lib/bot/jobs/hot-signal-sync.d.ts +8 -0
- package/dist/lib/bot/jobs/hot-signal-sync.js +2 -0
- package/dist/lib/bot/jobs/meeting-detect.d.ts +28 -0
- package/dist/lib/bot/jobs/meeting-detect.js +176 -0
- package/dist/lib/bot/jobs/meeting-rider.d.ts +53 -0
- package/dist/lib/bot/jobs/meeting-rider.js +284 -0
- package/dist/lib/bot/jobs/signal-to-lane.d.ts +32 -0
- package/dist/lib/bot/jobs/signal-to-lane.js +169 -0
- package/dist/lib/bot/local-jobs.d.ts +15 -2
- package/dist/lib/bot/local-jobs.js +24 -7
- package/dist/lib/bot/run.d.ts +7 -49
- package/dist/lib/bot/run.js +100 -4
- package/dist/lib/bot/status.d.ts +21 -0
- package/dist/lib/doctor/checks/bots.d.ts +6 -0
- package/dist/lib/doctor/checks/bots.js +47 -1
- package/dist/lib/hot-channels/adapters/meetings.d.ts +1 -0
- package/dist/lib/hot-channels/adapters/meetings.js +3 -1
- package/dist/lib/hot-channels/meeting-source-ref.d.ts +3 -0
- package/dist/lib/hot-channels/meeting-source-ref.js +7 -0
- package/dist/lib/hot-channels/schedule.js +2 -2
- package/dist/lib/hot-channels/signals-pass.d.ts +9 -32
- package/dist/lib/hot-channels/signals-pass.js +51 -17
- package/dist/lib/lanes/admission.d.ts +2 -0
- package/dist/lib/lanes/admission.js +4 -0
- package/dist/lib/lanes/approval-gate.js +4 -1
- package/dist/lib/lanes/links.d.ts +8 -1
- package/dist/lib/lanes/links.js +84 -3
- package/dist/lib/lanes/outcome.d.ts +154 -0
- package/dist/lib/lanes/outcome.js +151 -0
- package/dist/lib/lanes/reconcile.js +1 -0
- package/dist/lib/lanes/signal-brief-template.d.ts +7 -0
- package/dist/lib/lanes/signal-brief-template.js +40 -0
- package/dist/lib/lanes/signal-lane.d.ts +24 -0
- package/dist/lib/lanes/signal-lane.js +99 -0
- package/dist/lib/lanes/source-report.d.ts +37 -0
- package/dist/lib/lanes/source-report.js +66 -0
- package/dist/lib/lanes/spawn.d.ts +6 -0
- package/dist/lib/lanes/spawn.js +21 -0
- package/dist/lib/lanes/store.d.ts +3 -0
- package/dist/lib/lanes/store.js +64 -0
- package/dist/lib/lanes/story-ledger.d.ts +3 -0
- package/dist/lib/lanes/story-ledger.js +1 -0
- package/dist/lib/lanes/types.d.ts +137 -0
- package/dist/lib/lanes/types.js +17 -2
- package/dist/lib/lanes/watcher.js +4 -0
- package/dist/lib/live-meetings.d.ts +19 -0
- package/dist/lib/live-meetings.js +23 -0
- package/dist/lib/mesh/live/daemon/doctor.js +6 -7
- package/dist/lib/mesh/live/spool.d.ts +8 -0
- package/dist/lib/mesh/live/spool.js +59 -40
- package/dist/lib/plan-limit-nag.d.ts +30 -4
- package/dist/lib/plan-limit-nag.js +68 -17
- package/dist/lib/signals/engine.d.ts +4 -0
- package/dist/lib/signals/engine.js +25 -5
- package/dist/lib/signals/incremental.d.ts +42 -0
- package/dist/lib/signals/incremental.js +191 -0
- package/dist/lib/signals/prior-signals.d.ts +11 -0
- package/dist/lib/signals/prior-signals.js +44 -0
- package/dist/lib/signals/reply.js +6 -1
- package/dist/lib/signals/settings.d.ts +5 -0
- package/dist/lib/signals/settings.js +17 -0
- package/dist/lib/signals/steps/extract.d.ts +23 -2
- package/dist/lib/signals/steps/extract.js +142 -9
- package/dist/lib/signals/steps/types.d.ts +1 -0
- package/dist/lib/signals/write-guard.js +7 -0
- package/dist/utils/cognito-callback-port.d.ts +15 -0
- package/dist/utils/cognito-callback-port.js +51 -0
- package/dist/utils/cognito-config.js +2 -3
- package/dist/utils/cognito-session.d.ts +7 -6
- package/dist/utils/cognito-session.js +14 -14
- package/dist/utils/interactive-caller.d.ts +15 -0
- package/dist/utils/interactive-caller.js +70 -0
- package/dist/utils/login-browser.js +3 -1
- package/dist/utils/resident-package-preload.js +4 -0
- package/package.json +7 -7
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/** Source-independent action-item consumer for the personal Executive. */
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import { readBotConfig } from "../config.js";
|
|
6
|
+
import { botsRoot } from "../paths.js";
|
|
7
|
+
import { fleetProfileOwnerCompanies } from "../fleet-local.js";
|
|
8
|
+
import { fleetHotSignalSyncCompanies } from "../fleet-cron.js";
|
|
9
|
+
import { readActiveCompanySlug, readBuiltinJobsMarker, writeBuiltinJobsMarker } from "./hot-signal-sync.js";
|
|
10
|
+
import { isExecutiveBot } from "./meeting-rider.js";
|
|
11
|
+
import { readLocalJobsStrict, writeLocalJobs, withJobsLock } from "../local-jobs.js";
|
|
12
|
+
import { emitBotEvent } from "../events.js";
|
|
13
|
+
import { isLanesError } from "../../lanes/errors.js";
|
|
14
|
+
import { lookupSignal } from "../../lanes/signal-lane.js";
|
|
15
|
+
export const SIGNAL_TO_LANE_JOB_KIND = "signal-to-lane";
|
|
16
|
+
export function signalLaneCompanies(config) {
|
|
17
|
+
return fleetHotSignalSyncCompanies(config, readActiveCompanySlug(config.hqRoot) ?? null, fleetProfileOwnerCompanies(config.agentUid));
|
|
18
|
+
}
|
|
19
|
+
export function ensureSignalToLaneJobs(dir, config, companies = signalLaneCompanies(config), home) {
|
|
20
|
+
if (!isExecutiveBot(config, home))
|
|
21
|
+
return [];
|
|
22
|
+
return withJobsLock(dir, () => {
|
|
23
|
+
const jobs = readLocalJobsStrict(dir);
|
|
24
|
+
const marker = readBuiltinJobsMarker(dir);
|
|
25
|
+
for (const company of new Set(companies)) {
|
|
26
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(company) || company === "personal")
|
|
27
|
+
continue;
|
|
28
|
+
const key = `${SIGNAL_TO_LANE_JOB_KIND}:${company}`;
|
|
29
|
+
if (marker.registered[key])
|
|
30
|
+
continue;
|
|
31
|
+
if (!jobs.some(job => job.context?.jobKind === SIGNAL_TO_LANE_JOB_KIND && job.context.company === company)) {
|
|
32
|
+
jobs.push({ jobId: `job_${randomUUID().replaceAll("-", "").slice(0, 20)}`,
|
|
33
|
+
prompt: "Open a conductor lane from an action item in-process.", timezone: "UTC",
|
|
34
|
+
trigger: { kind: "event", event: "signal.written", filter: { type: "action_item", company } },
|
|
35
|
+
context: { jobKind: SIGNAL_TO_LANE_JOB_KIND, company }, createdAt: new Date().toISOString() });
|
|
36
|
+
}
|
|
37
|
+
marker.registered[key] = new Date().toISOString();
|
|
38
|
+
}
|
|
39
|
+
writeLocalJobs(dir, jobs);
|
|
40
|
+
writeBuiltinJobsMarker(dir, marker);
|
|
41
|
+
return jobs.filter(job => job.context?.jobKind === SIGNAL_TO_LANE_JOB_KIND);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/** Only new locally extracted signals reach here; cloud upload/garden is separate. */
|
|
45
|
+
export function emitExecutiveSignalWritten(hqRoot, company, payload, root = botsRoot(), home) {
|
|
46
|
+
if (!fs.existsSync(root))
|
|
47
|
+
return false;
|
|
48
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
49
|
+
if (!entry.isDirectory())
|
|
50
|
+
continue;
|
|
51
|
+
const dir = path.join(root, entry.name);
|
|
52
|
+
const config = readBotConfig(dir);
|
|
53
|
+
if (!config || typeof config.hqRoot !== "string" || path.resolve(config.hqRoot) !== path.resolve(hqRoot) || !isExecutiveBot(config, home))
|
|
54
|
+
continue;
|
|
55
|
+
if (!signalLaneCompanies(config).includes(company))
|
|
56
|
+
continue;
|
|
57
|
+
emitBotEvent(dir, "signal.written", { ...payload, company }, new Date(), prior => prior.event === "signal.written" && prior.payload.company === company && prior.payload.signal_id === payload.signal_id);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
function publicationDir(hqRoot, company) {
|
|
63
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(company))
|
|
64
|
+
throw new Error("Invalid signal publication company");
|
|
65
|
+
return path.join(hqRoot, "workspace", "signal-publications", company);
|
|
66
|
+
}
|
|
67
|
+
/** Persist intent before publishing a new signal file; ordinary redo creates none. */
|
|
68
|
+
export function prepareSignalPublication(hqRoot, company, payload) {
|
|
69
|
+
const dir = publicationDir(hqRoot, company);
|
|
70
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
71
|
+
const file = path.join(dir, createHash("sha256").update(payload.signal_id).digest("hex") + ".json");
|
|
72
|
+
const tmp = `${file}.${randomUUID()}.tmp`;
|
|
73
|
+
fs.writeFileSync(tmp, JSON.stringify(payload), { mode: 0o600 });
|
|
74
|
+
fs.renameSync(tmp, file);
|
|
75
|
+
}
|
|
76
|
+
/** A pre-append crash retries; a post-append crash uses the event journal's dedupe. */
|
|
77
|
+
export function flushSignalPublications(hqRoot, company, emit = emitExecutiveSignalWritten) {
|
|
78
|
+
const dir = publicationDir(hqRoot, company);
|
|
79
|
+
if (!fs.existsSync(dir))
|
|
80
|
+
return;
|
|
81
|
+
for (const name of fs.readdirSync(dir).filter(name => /^[a-f0-9]{64}\.json$/.test(name))) {
|
|
82
|
+
const file = path.join(dir, name);
|
|
83
|
+
let payload;
|
|
84
|
+
try {
|
|
85
|
+
payload = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (error.code === "ENOENT")
|
|
89
|
+
continue;
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload) ||
|
|
93
|
+
["signal_id", "type", "source_ref", "audience_key", "origin"].some(key => typeof payload[key] !== "string")) {
|
|
94
|
+
throw new Error("Invalid signal publication intent");
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
lookupSignal(hqRoot, company, payload.signal_id);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (isLanesError(error) && error.error === "signal_not_found")
|
|
101
|
+
continue;
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
if (emit(hqRoot, company, payload)) {
|
|
105
|
+
try {
|
|
106
|
+
fs.unlinkSync(file);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (error.code !== "ENOENT")
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const createDefault = async (input) => {
|
|
116
|
+
// Load the CLI admission graph only when a signal actually needs a lane.
|
|
117
|
+
const { createLaneFromSignal } = await import("../../../commands/lanes.js");
|
|
118
|
+
return createLaneFromSignal(input);
|
|
119
|
+
};
|
|
120
|
+
export async function runSignalToLane(input) {
|
|
121
|
+
if (!isExecutiveBot(input.config, input.home))
|
|
122
|
+
return { status: "skipped-precondition", output: "Not the Executive" };
|
|
123
|
+
const payload = input.payload;
|
|
124
|
+
const company = input.company;
|
|
125
|
+
if (!payload || payload.type !== "action_item" || typeof payload.signal_id !== "string" || !payload.signal_id.trim() ||
|
|
126
|
+
typeof company !== "string" || payload.company !== company || !(input.companies ?? signalLaneCompanies(input.config)).includes(company)) {
|
|
127
|
+
return { status: "skipped-precondition", output: "Signal outside the served company or action-item scope" };
|
|
128
|
+
}
|
|
129
|
+
// Persist deferred IDs before acknowledging; the next matching event retries.
|
|
130
|
+
const file = path.join(input.dir, "signal-to-lane-pending.json");
|
|
131
|
+
const read = () => {
|
|
132
|
+
try {
|
|
133
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
134
|
+
if (!value || typeof value !== "object" || Array.isArray(value) ||
|
|
135
|
+
Object.values(value).some(ids => !Array.isArray(ids) || ids.some(id => typeof id !== "string"))) {
|
|
136
|
+
throw new Error("Invalid pending signal lane state");
|
|
137
|
+
}
|
|
138
|
+
return Object.assign(Object.create(null), value);
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
if (err.code === "ENOENT")
|
|
142
|
+
return Object.create(null);
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const update = (change) => withJobsLock(input.dir, () => {
|
|
147
|
+
const pending = read();
|
|
148
|
+
change(pending);
|
|
149
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
150
|
+
fs.writeFileSync(tmp, JSON.stringify(pending), { mode: 0o600 });
|
|
151
|
+
fs.renameSync(tmp, file);
|
|
152
|
+
return pending;
|
|
153
|
+
});
|
|
154
|
+
const pending = update(p => { p[company] = [...new Set([...(p[company] ?? []), payload.signal_id])]; });
|
|
155
|
+
for (const signal_id of pending[company] ?? []) {
|
|
156
|
+
try {
|
|
157
|
+
const result = await (input.create ?? createDefault)({ hqRoot: input.config.hqRoot, company, signal_id });
|
|
158
|
+
input.log(JSON.stringify(result));
|
|
159
|
+
update(p => { p[company] = (p[company] ?? []).filter(id => id !== signal_id); });
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
if (!isLanesError(err) || err.error !== "no_conductor_link")
|
|
163
|
+
throw err;
|
|
164
|
+
input.log(JSON.stringify({ error: "no_conductor_link", company, signal_id }));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { status: "succeeded", output: "Signal lane processing finished; deferred signals retry on the next matching event" };
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=signal-to-lane.js.map
|
|
@@ -23,7 +23,12 @@ export interface LocalJob {
|
|
|
23
23
|
jobId: string;
|
|
24
24
|
prompt: string;
|
|
25
25
|
/** Standard 5-field cron, evaluated in `timezone`. */
|
|
26
|
-
cron
|
|
26
|
+
cron?: string;
|
|
27
|
+
trigger?: {
|
|
28
|
+
kind: "event";
|
|
29
|
+
event: string;
|
|
30
|
+
filter?: Record<string, string>;
|
|
31
|
+
};
|
|
27
32
|
timezone: string;
|
|
28
33
|
context?: Record<string, unknown>;
|
|
29
34
|
createdAt: string;
|
|
@@ -45,6 +50,12 @@ export interface LocalJob {
|
|
|
45
50
|
runCount?: number;
|
|
46
51
|
}
|
|
47
52
|
export declare function readLocalJobs(dir: string): LocalJob[];
|
|
53
|
+
/**
|
|
54
|
+
* `jobs.json` for a write that adds a job: a missing file is an empty list,
|
|
55
|
+
* but a file that exists and cannot be read or parsed throws, so adding a job
|
|
56
|
+
* never writes back a list that drops every other job.
|
|
57
|
+
*/
|
|
58
|
+
export declare function readLocalJobsStrict(dir: string): LocalJob[];
|
|
48
59
|
export declare function writeLocalJobs(dir: string, jobs: LocalJob[]): void;
|
|
49
60
|
/**
|
|
50
61
|
* Run a read-change-write of `jobs.json` under an exclusive lock. The running
|
|
@@ -74,9 +85,11 @@ export declare function parseCron(expr: string): CronMatcher | null;
|
|
|
74
85
|
export declare function latestDueFire(job: LocalJob, now: Date): Date | null;
|
|
75
86
|
/** The next time this job is due after `now`, within a week; null otherwise. */
|
|
76
87
|
export declare function nextDueFire(job: LocalJob, now: Date): Date | null;
|
|
88
|
+
export declare function toJobRecord(job: LocalJob, now?: Date): BotJobRecord;
|
|
77
89
|
export declare function addLocalJob(dir: string, input: {
|
|
78
90
|
prompt: string;
|
|
79
|
-
cron
|
|
91
|
+
cron?: string;
|
|
92
|
+
trigger?: LocalJob["trigger"];
|
|
80
93
|
timezone: string;
|
|
81
94
|
context?: Record<string, unknown>;
|
|
82
95
|
}, now?: Date): LocalJob;
|
|
@@ -22,6 +22,7 @@ import * as fs from "node:fs";
|
|
|
22
22
|
import * as path from "node:path";
|
|
23
23
|
import { randomUUID } from "node:crypto";
|
|
24
24
|
import { BotApiError } from "./api.js";
|
|
25
|
+
import { ackBotEvent } from "./events.js";
|
|
25
26
|
const LOCAL_JOBS_FILE = "jobs.json";
|
|
26
27
|
const LOCAL_JOB_MESSAGE_PREFIX = "localjob-";
|
|
27
28
|
/** How far back a missed fire is still looked for (one week). */
|
|
@@ -47,7 +48,7 @@ export function readLocalJobs(dir) {
|
|
|
47
48
|
* but a file that exists and cannot be read or parsed throws, so adding a job
|
|
48
49
|
* never writes back a list that drops every other job.
|
|
49
50
|
*/
|
|
50
|
-
function
|
|
51
|
+
export function readLocalJobsStrict(dir) {
|
|
51
52
|
let text;
|
|
52
53
|
try {
|
|
53
54
|
text = fs.readFileSync(localJobsPath(dir), "utf8");
|
|
@@ -270,6 +271,8 @@ function scheduleOrigin(job) {
|
|
|
270
271
|
return Date.parse(job.scheduledFrom ?? job.createdAt);
|
|
271
272
|
}
|
|
272
273
|
export function latestDueFire(job, now) {
|
|
274
|
+
if (job.trigger || !job.cron)
|
|
275
|
+
return null;
|
|
273
276
|
const interval = rateIntervalMs(job.cron);
|
|
274
277
|
if (interval) {
|
|
275
278
|
const origin = scheduleOrigin(job);
|
|
@@ -294,6 +297,8 @@ export function latestDueFire(job, now) {
|
|
|
294
297
|
}
|
|
295
298
|
/** The next time this job is due after `now`, within a week; null otherwise. */
|
|
296
299
|
export function nextDueFire(job, now) {
|
|
300
|
+
if (job.trigger || !job.cron)
|
|
301
|
+
return null;
|
|
297
302
|
const interval = rateIntervalMs(job.cron);
|
|
298
303
|
if (interval) {
|
|
299
304
|
const origin = scheduleOrigin(job);
|
|
@@ -313,13 +318,13 @@ export function nextDueFire(job, now) {
|
|
|
313
318
|
return null;
|
|
314
319
|
}
|
|
315
320
|
// ─── The job list (what `hq bot jobs` reads and writes) ──────────────────────
|
|
316
|
-
function toJobRecord(job, now = new Date()) {
|
|
321
|
+
export function toJobRecord(job, now = new Date()) {
|
|
317
322
|
const next = nextDueFire(job, now);
|
|
318
323
|
return {
|
|
319
324
|
jobId: job.jobId,
|
|
320
325
|
prompt: job.prompt,
|
|
321
326
|
status: "active",
|
|
322
|
-
schedule: { kind: "recurring", cron: job.cron, timezone: job.timezone },
|
|
327
|
+
...(job.trigger ? { trigger: job.trigger } : { schedule: { kind: "recurring", cron: job.cron, timezone: job.timezone } }),
|
|
323
328
|
...(job.context ? { context: job.context } : {}),
|
|
324
329
|
createdAt: job.createdAt,
|
|
325
330
|
...(next ? { nextRunAt: next.toISOString() } : {}),
|
|
@@ -329,17 +334,24 @@ function toJobRecord(job, now = new Date()) {
|
|
|
329
334
|
};
|
|
330
335
|
}
|
|
331
336
|
export function addLocalJob(dir, input, now = new Date()) {
|
|
332
|
-
if (!
|
|
337
|
+
if (input.trigger && (!input.trigger.event.trim() || input.trigger.kind !== "event" || input.cron))
|
|
338
|
+
throw new Error("An event job needs an event and no cron schedule.");
|
|
339
|
+
if (!input.trigger && (!input.cron || !isRunnableSchedule(input.cron)))
|
|
333
340
|
throw new Error(`"${input.cron}" is not a schedule this bot can run.`);
|
|
334
341
|
const job = {
|
|
335
342
|
jobId: `job_${randomUUID().replace(/-/g, "").slice(0, 20)}`,
|
|
336
343
|
prompt: input.prompt,
|
|
337
|
-
cron: input.cron,
|
|
344
|
+
...(input.trigger ? { trigger: input.trigger } : { cron: input.cron }),
|
|
338
345
|
timezone: input.timezone,
|
|
339
346
|
...(input.context ? { context: input.context } : {}),
|
|
340
347
|
createdAt: now.toISOString(),
|
|
341
348
|
};
|
|
342
|
-
withJobsLock(dir, () =>
|
|
349
|
+
withJobsLock(dir, () => {
|
|
350
|
+
const jobs = readLocalJobsStrict(dir);
|
|
351
|
+
if (jobs.length >= MAX_LOCAL_JOBS)
|
|
352
|
+
throw new BotApiError(409, "local jobs", "JOB_QUOTA_EXCEEDED", `at most ${MAX_LOCAL_JOBS} jobs`);
|
|
353
|
+
writeLocalJobs(dir, [...jobs, job]);
|
|
354
|
+
});
|
|
343
355
|
return job;
|
|
344
356
|
}
|
|
345
357
|
/**
|
|
@@ -350,7 +362,7 @@ export function ensureLocalJob(dir, matches, input, now = new Date()) {
|
|
|
350
362
|
if (!isRunnableSchedule(input.cron))
|
|
351
363
|
throw new Error(`"${input.cron}" is not a schedule this bot can run.`);
|
|
352
364
|
return withJobsLock(dir, () => {
|
|
353
|
-
const jobs =
|
|
365
|
+
const jobs = readLocalJobsStrict(dir);
|
|
354
366
|
const existing = jobs.find(matches);
|
|
355
367
|
if (existing)
|
|
356
368
|
return { job: existing, created: false };
|
|
@@ -386,6 +398,7 @@ schedule, now = new Date()) {
|
|
|
386
398
|
timezone: schedule.timezone ?? jobs[index].timezone,
|
|
387
399
|
scheduledFrom: now.toISOString(),
|
|
388
400
|
};
|
|
401
|
+
delete updated.trigger;
|
|
389
402
|
jobs[index] = updated;
|
|
390
403
|
writeLocalJobs(dir, jobs);
|
|
391
404
|
return updated;
|
|
@@ -517,6 +530,10 @@ export function withLocalJobs(api, dir, clock = () => new Date()) {
|
|
|
517
530
|
return [...cloud, ...takeDueLocalJobRuns(dir, clock())];
|
|
518
531
|
};
|
|
519
532
|
local.ackInbox = async (agentUid, messageId) => {
|
|
533
|
+
if (messageId.startsWith("localjob-event-")) {
|
|
534
|
+
ackBotEvent(dir, messageId);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
520
537
|
if (isLocalJobMessage(messageId))
|
|
521
538
|
return;
|
|
522
539
|
await api.ackInbox(agentUid, messageId);
|
package/dist/lib/bot/run.d.ts
CHANGED
|
@@ -1,52 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The resident bot process: `hq bot run <name>` (local-bots US-003/US-004).
|
|
3
|
-
*
|
|
4
|
-
* - pid lock in ~/.hq/bots/<name>/pid; status.json is the observable state
|
|
5
|
-
* - refuses to start unless the creds file names this bot's agt_ AND the
|
|
6
|
-
* server-side record is owned by the configured owner
|
|
7
|
-
* - heartbeat every 30s (POST /v1/agents/{uid}/heartbeat, agent JWT)
|
|
8
|
-
* - inbox poll every 2s: DMs go to ONE headless model turn (resumed by
|
|
9
|
-
* session id); a new session (restart, or a resume id the runtime no longer
|
|
10
|
-
* has) is given the recent DM thread as a "Recent messages:" block so a
|
|
11
|
-
* one-word reply still has the question it answers; the reply is sent as
|
|
12
|
-
* the bot; the message is acked only after the reply is sent; processed ids
|
|
13
|
-
* persist so a crash between reply and ack never answers twice
|
|
14
|
-
* - any correspondent can message the bot; each sender has a separate session
|
|
15
|
-
* - first start only: the intro DM, then (when bot.json has `kickoff`) ONE
|
|
16
|
-
* model turn on that prompt as if the owner sent it, answered in the DM, so
|
|
17
|
-
* a guided bot starts working without waiting for the owner to type
|
|
18
|
-
* - job items (channel "job"): a scheduled run, fired by the bot's own loop
|
|
19
|
-
* from its jobs.json (lib/bot/local-jobs.ts) or, for a hosted agent, by
|
|
20
|
-
* the cloud. No system scheduler is used on this computer. The run is one model turn
|
|
21
|
-
* in its own session (one per jobId), reported in the owner's DM with a
|
|
22
|
-
* "scheduled job" header through the same split/recovery path as every other
|
|
23
|
-
* reply, and reported back to the cloud EXACTLY ONCE — nothing else clears
|
|
24
|
-
* the job's in-progress flag. A run whose staleAfter has passed still runs,
|
|
25
|
-
* once, labelled a catch-up; older pending runs of the same job are reported
|
|
26
|
-
* skipped-precondition instead of piling up.
|
|
27
|
-
* - delivered room items (channels / group chats) are eligible for replies; claimed
|
|
28
|
-
* first (first-claim-wins), one session per room, reply posted in the room
|
|
29
|
-
* - conversations run side by side: up to MAX_PARALLEL_TURNS turns at once,
|
|
30
|
-
* one lane per model session (one DM correspondent, or one room). Messages
|
|
31
|
-
* in the same lane run in order, so a session is never resumed by two turns
|
|
32
|
-
* at once and a thread's answers never arrive out of sequence.
|
|
33
|
-
* - while a turn runs, each finished assistant message is posted as its own
|
|
34
|
-
* message (progress.ts); turns have no time limit
|
|
35
|
-
* - every received message gets a reply: a turn that fails after the model
|
|
36
|
-
* started (error, crash, owner stop, restart) posts one "did not finish"
|
|
37
|
-
* message and is NOT run again — it may already have done outward things.
|
|
38
|
-
* inflight.json marks the turn in progress so a restart can say so.
|
|
39
|
-
* - a turn that fails because the coding tool's sign-in no longer works
|
|
40
|
-
* (runtime-sign-in.ts) is not a failure of the message: the bot says once
|
|
41
|
-
* per conversation that it needs a sign-in, keeps the message un-acked,
|
|
42
|
-
* holds new turns, and tries again every SIGN_IN_RETRY_MS (and at once
|
|
43
|
-
* after a restart). A turn that works clears it.
|
|
44
|
-
* - failures before the model starts (missing binary) retry with backoff;
|
|
45
|
-
* 5 model failures in 10 minutes → state `failed` and a clean exit (launchd
|
|
46
|
-
* does not respawn a clean exit). Owner stops never count.
|
|
47
|
-
*
|
|
48
|
-
* Everything with a side effect is injectable so the loop is unit-testable.
|
|
49
|
-
*/
|
|
50
1
|
import { type OwnerContext } from "./owner-context.js";
|
|
51
2
|
import { type BotApi, type InboxItem } from "./api.js";
|
|
52
3
|
import type { BotConfig, BotKind } from "./config.js";
|
|
@@ -57,6 +8,7 @@ import { type BotRuntime, type RuntimeTurnInput, type RuntimeTurnResult } from "
|
|
|
57
8
|
import { type BotSessionScope } from "./paths.js";
|
|
58
9
|
import { type DecisionDmClient } from "./decisions.js";
|
|
59
10
|
import type { VaultObjectDownloader } from "../agent-kit/attachment-download.js";
|
|
11
|
+
import type { JobRunCompletion } from "./api.js";
|
|
60
12
|
import { type RuntimeSignInCheck } from "./runtime-sign-in.js";
|
|
61
13
|
import { type HeartbeatAgentUidSource, type HeartbeatCredsSource, type HeartbeatIdentityMismatchReporter } from "../heartbeat-identity.js";
|
|
62
14
|
import { type BotState, type BotStatusFile } from "./status.js";
|
|
@@ -325,6 +277,12 @@ export declare function inboxLaneKey(item: InboxItem, config: Pick<BotConfig, "o
|
|
|
325
277
|
*/
|
|
326
278
|
export declare function dmTurnSessionScope(item: Pick<InboxItem, "fromPersonUid" | "text">, ownerUid: string): BotSessionScope;
|
|
327
279
|
export declare function preflightCreds(dir: string, config: BotConfig): string | null;
|
|
280
|
+
/** Register source-independent in-process jobs by their context.jobKind. */
|
|
281
|
+
export declare const builtinEventHandlers: Record<string, (input: {
|
|
282
|
+
dir: string;
|
|
283
|
+
config: BotConfig;
|
|
284
|
+
item: InboxItem;
|
|
285
|
+
}) => Promise<JobRunCompletion>>;
|
|
328
286
|
export declare function runBot(deps: BotRunDeps): Promise<BotHandle>;
|
|
329
287
|
/** True for a setup note the desktop app sent, not words the person typed. */
|
|
330
288
|
export declare function isAppSetupNote(text: string): boolean;
|
package/dist/lib/bot/run.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ensureSignalToLaneJobs, runSignalToLane, SIGNAL_TO_LANE_JOB_KIND } from "./jobs/signal-to-lane.js";
|
|
1
2
|
/**
|
|
2
3
|
* The resident bot process: `hq bot run <name>` (local-bots US-003/US-004).
|
|
3
4
|
*
|
|
@@ -47,6 +48,9 @@
|
|
|
47
48
|
*
|
|
48
49
|
* Everything with a side effect is injectable so the loop is unit-testable.
|
|
49
50
|
*/
|
|
51
|
+
import { createMeetingRider, defaultMeetingRiderDeps, ensureMeetingRiderJob, MEETING_RIDER_JOB_KIND } from "./jobs/meeting-rider.js";
|
|
52
|
+
import { executiveRiderEnabled, isExecutiveBot } from "./executive-settings.js";
|
|
53
|
+
import { createMeetingDetect, defaultMeetingDetectDeps, resolveDetectedMeeting } from "./jobs/meeting-detect.js";
|
|
50
54
|
import { botAuthEnv, describeBotAuth, formatBotAuth } from "./runtime-auth.js";
|
|
51
55
|
import { ownerContextBlock } from "./owner-context.js";
|
|
52
56
|
import { randomUUID } from "node:crypto";
|
|
@@ -86,6 +90,7 @@ import { buildJobTurnPrompt, isCatchUpRun, isJobItem, jobReportHeader, jobRunOut
|
|
|
86
90
|
import { botJobQuietOptionEnabled } from "./job-quiet-flag.js";
|
|
87
91
|
import { currentHotSignalSyncCompany, hotSignalSyncJobCompany, readActiveCompanySlug, removeStaleHotSignalSyncJobs } from "./jobs/hot-signal-sync.js";
|
|
88
92
|
import { isLocalJobMessage, isLocalJobRetryMessage, localJobRetryMessageId, readLocalJobs } from "./local-jobs.js";
|
|
93
|
+
import { drainBotEvents } from "./events.js";
|
|
89
94
|
import { clearPendingJobCompletion, isCompletionDue, isCompletionExpired, isCompletionForOtherAgent, readPendingJobCompletions, recordPendingJobCompletion } from "./job-completions.js";
|
|
90
95
|
import { SIGN_IN_CHECK_FRESH_MS, SIGN_IN_CHECK_MS, SIGN_IN_RETRY_MS, signInNeededText, signedOutExcerpt, } from "./runtime-sign-in.js";
|
|
91
96
|
import { checkRuntimeSignIn } from "./runtime-sign-in-check.js";
|
|
@@ -407,6 +412,8 @@ export function preflightCreds(dir, config) {
|
|
|
407
412
|
}
|
|
408
413
|
return null;
|
|
409
414
|
}
|
|
415
|
+
/** Register source-independent in-process jobs by their context.jobKind. */
|
|
416
|
+
export const builtinEventHandlers = Object.create(null);
|
|
410
417
|
export async function runBot(deps) {
|
|
411
418
|
const now = deps.now ?? (() => new Date());
|
|
412
419
|
const sleep = deps.sleep ?? defaultSleep;
|
|
@@ -414,6 +421,23 @@ export async function runBot(deps) {
|
|
|
414
421
|
const log = deps.log ?? createBotLogger(deps.dir, now);
|
|
415
422
|
const pid = deps.pid ?? process.pid;
|
|
416
423
|
const { dir, config, api, runtime } = deps;
|
|
424
|
+
const meetingRider = createMeetingRider(defaultMeetingRiderDeps(config, dir, (message) => log("info", message)));
|
|
425
|
+
const meetingDetectDeps = defaultMeetingDetectDeps(config, dir, (message) => log("info", message));
|
|
426
|
+
const meetingDetect = createMeetingDetect(meetingDetectDeps);
|
|
427
|
+
const eventHandlers = Object.create(builtinEventHandlers);
|
|
428
|
+
eventHandlers[MEETING_RIDER_JOB_KIND] = async ({ item }) => {
|
|
429
|
+
if (!isExecutiveBot(config))
|
|
430
|
+
return { status: "skipped-precondition", output: "Not the Executive" };
|
|
431
|
+
if (!executiveRiderEnabled())
|
|
432
|
+
return { status: "skipped-precondition", output: "rider disabled" };
|
|
433
|
+
meetingRider.start(await resolveDetectedMeeting(config.hqRoot, typeof item.context === "object" ? item.context?.payload : undefined, meetingDetectDeps.list));
|
|
434
|
+
return { status: "succeeded", output: "Meeting rider accepted" };
|
|
435
|
+
};
|
|
436
|
+
eventHandlers[SIGNAL_TO_LANE_JOB_KIND] = async ({ item }) => runSignalToLane({
|
|
437
|
+
dir, config, payload: typeof item.context === "object" ? item.context?.payload : undefined,
|
|
438
|
+
company: typeof item.context === "object" ? item.context?.company : undefined,
|
|
439
|
+
log: message => log("info", message),
|
|
440
|
+
});
|
|
417
441
|
const runTurn = deps.runTurn ?? ((rt, input) => runRuntimeTurn(rt, input));
|
|
418
442
|
const inboxIntervalMs = deps.inboxIntervalMs ?? INBOX_POLL_INTERVAL_MS;
|
|
419
443
|
const inboxPollBackoffEnabled = deps.inboxPollBackoffEnabled ?? createInboxPollBackoffFlagReader(config);
|
|
@@ -554,9 +578,13 @@ export async function runBot(deps) {
|
|
|
554
578
|
...(deps.isProcessAlive ? { isProcessAlive: deps.isProcessAlive } : {}),
|
|
555
579
|
});
|
|
556
580
|
const finish = async (final, code, reason, beforeExit) => {
|
|
557
|
-
if (state === "stopped" || state === "failed")
|
|
581
|
+
if (state === "stopped" || state === "failed") {
|
|
582
|
+
await done;
|
|
558
583
|
return;
|
|
584
|
+
}
|
|
559
585
|
state = final;
|
|
586
|
+
await meetingDetect.stop();
|
|
587
|
+
await meetingRider.stop();
|
|
560
588
|
closeInboxWakeWatcher();
|
|
561
589
|
setStatus({ state: final, ...(final === "failed" ? { lastError: reason.slice(0, 400) } : {}) });
|
|
562
590
|
log(final === "failed" ? "error" : "info", `bot ${final}: ${reason}`);
|
|
@@ -1031,6 +1059,8 @@ export async function runBot(deps) {
|
|
|
1031
1059
|
activeInboxWait?.abort();
|
|
1032
1060
|
};
|
|
1033
1061
|
const waitForInbox = async (delayMs, adaptive, useMqttWake = false) => {
|
|
1062
|
+
if (readLocalJobs(dir).some((job) => job.trigger))
|
|
1063
|
+
delayMs = Math.min(delayMs, inboxIntervalMs);
|
|
1034
1064
|
if (!adaptive) {
|
|
1035
1065
|
await sleep(delayMs);
|
|
1036
1066
|
return;
|
|
@@ -1040,9 +1070,14 @@ export async function runBot(deps) {
|
|
|
1040
1070
|
while (remainingMs > 0 && !stopping && state === "running") {
|
|
1041
1071
|
const controller = new AbortController();
|
|
1042
1072
|
activeInboxWait = controller;
|
|
1043
|
-
const chunkMs =
|
|
1073
|
+
const chunkMs = Math.min(inboxIntervalMs, remainingMs);
|
|
1044
1074
|
if (useMqttWake) {
|
|
1045
|
-
await getInboxWakeWatcher().wait(chunkMs, controller.signal);
|
|
1075
|
+
const wake = await getInboxWakeWatcher().wait(chunkMs, controller.signal);
|
|
1076
|
+
if (wake === "wake") {
|
|
1077
|
+
if (activeInboxWait === controller)
|
|
1078
|
+
activeInboxWait = null;
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1046
1081
|
}
|
|
1047
1082
|
else {
|
|
1048
1083
|
await sleep(chunkMs, controller.signal);
|
|
@@ -1053,6 +1088,9 @@ export async function runBot(deps) {
|
|
|
1053
1088
|
break;
|
|
1054
1089
|
if (hasPromotionHold(dir))
|
|
1055
1090
|
break;
|
|
1091
|
+
// The first event job can be added while an otherwise idle bot waits.
|
|
1092
|
+
if (readLocalJobs(dir).some((job) => job.trigger))
|
|
1093
|
+
break;
|
|
1056
1094
|
if (observedGeneration !== inboxWaitGeneration || controller.signal.aborted) {
|
|
1057
1095
|
observedGeneration = inboxWaitGeneration;
|
|
1058
1096
|
remainingMs = inboxIntervalMs;
|
|
@@ -2017,6 +2055,31 @@ export async function runBot(deps) {
|
|
|
2017
2055
|
const id = item.messageId;
|
|
2018
2056
|
const { job, jobId } = target;
|
|
2019
2057
|
const peerUid = target.peerUid;
|
|
2058
|
+
const eventHandler = item.messageId.startsWith("localjob-event-") && typeof job.context === "object" && typeof job.context?.jobKind === "string"
|
|
2059
|
+
? eventHandlers[job.context.jobKind] : undefined;
|
|
2060
|
+
if (eventHandler) {
|
|
2061
|
+
// Recoverable built-ins persist intent before acknowledging the event.
|
|
2062
|
+
// Signal lane creation is idempotent, so a pre-ack crash can replay.
|
|
2063
|
+
const durableEvent = typeof job.context === "object" && (job.context?.jobKind === MEETING_RIDER_JOB_KIND || job.context?.jobKind === SIGNAL_TO_LANE_JOB_KIND);
|
|
2064
|
+
if (!durableEvent)
|
|
2065
|
+
markProcessed(dir, id, now().toISOString());
|
|
2066
|
+
let completion;
|
|
2067
|
+
try {
|
|
2068
|
+
completion = await eventHandler({ dir, config, item });
|
|
2069
|
+
// Durable intent is stored before acknowledging; a pre-effect crash replays.
|
|
2070
|
+
if (durableEvent)
|
|
2071
|
+
markProcessed(dir, id, now().toISOString());
|
|
2072
|
+
}
|
|
2073
|
+
catch (error) {
|
|
2074
|
+
if (durableEvent)
|
|
2075
|
+
throw error;
|
|
2076
|
+
completion = { status: "failed", output: error instanceof Error ? error.message : String(error) };
|
|
2077
|
+
}
|
|
2078
|
+
await completeJobRun(jobId, id, completion);
|
|
2079
|
+
await api.ackInbox(config.agentUid, id);
|
|
2080
|
+
log("info", `event job ${jobId} run ${id} ${completion.status}`);
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2020
2083
|
// An older pending run of this job: a newer one runs instead. Reported so
|
|
2021
2084
|
// the cloud releases the slot and the history says what happened.
|
|
2022
2085
|
if (supersededJobRuns.has(id)) {
|
|
@@ -2821,6 +2884,21 @@ export async function runBot(deps) {
|
|
|
2821
2884
|
// up below: end here, before the kickoff can send a "needs a sign-in" DM.
|
|
2822
2885
|
if (stopping)
|
|
2823
2886
|
return await stoppedDuringStartup();
|
|
2887
|
+
try {
|
|
2888
|
+
ensureSignalToLaneJobs(dir, config);
|
|
2889
|
+
}
|
|
2890
|
+
catch {
|
|
2891
|
+
log("warn", "Signal-to-lane registration failed");
|
|
2892
|
+
}
|
|
2893
|
+
if (isExecutiveBot(config)) {
|
|
2894
|
+
try {
|
|
2895
|
+
if (ensureMeetingRiderJob(dir) && executiveRiderEnabled())
|
|
2896
|
+
meetingRider.resume();
|
|
2897
|
+
}
|
|
2898
|
+
catch {
|
|
2899
|
+
log("warn", "Meeting rider registration or recovery failed");
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2824
2902
|
// The kickoff runs alongside the heartbeat, before the first inbox poll, so
|
|
2825
2903
|
// an owner message sent meanwhile is answered after it in the same session.
|
|
2826
2904
|
const kickoff = kickoffAfterIntro
|
|
@@ -2937,6 +3015,14 @@ export async function runBot(deps) {
|
|
|
2937
3015
|
kickCompletionRetries();
|
|
2938
3016
|
// A failed signals run's automatic retry, once due (no model turn).
|
|
2939
3017
|
enqueueDueSignalsRetries();
|
|
3018
|
+
if (isExecutiveBot(config)) {
|
|
3019
|
+
try {
|
|
3020
|
+
meetingDetect.tick();
|
|
3021
|
+
}
|
|
3022
|
+
catch {
|
|
3023
|
+
log("warn", "meeting detection settings unreadable");
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
2940
3026
|
// Holding after repeated model failures: no inbox pulls until the
|
|
2941
3027
|
// cooldown is over (the update watcher above keeps running meanwhile).
|
|
2942
3028
|
if (modelHoldUntil > now().getTime()) {
|
|
@@ -2945,6 +3031,15 @@ export async function runBot(deps) {
|
|
|
2945
3031
|
}
|
|
2946
3032
|
if (modelHoldUntil)
|
|
2947
3033
|
liftModelHold();
|
|
3034
|
+
// Local events are available even when the cloud inbox is unavailable.
|
|
3035
|
+
try {
|
|
3036
|
+
for (const item of drainBotEvents(dir))
|
|
3037
|
+
enqueue(item);
|
|
3038
|
+
pump();
|
|
3039
|
+
}
|
|
3040
|
+
catch (error) {
|
|
3041
|
+
log("warn", `event drain failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3042
|
+
}
|
|
2948
3043
|
// While the tool says it is signed out, ask it again on a timer (cheap,
|
|
2949
3044
|
// no turn): the moment it is signed in, the held messages run.
|
|
2950
3045
|
if (signInIssue && busyLanes.size === 0 && now().getTime() >= nextSignInCheckAt) {
|
|
@@ -3002,7 +3097,8 @@ export async function runBot(deps) {
|
|
|
3002
3097
|
log("warn", `inbox poll failed: ${message}`);
|
|
3003
3098
|
if (stopping || state !== "running")
|
|
3004
3099
|
break;
|
|
3005
|
-
|
|
3100
|
+
const retryDelay = apiPollDelayMs({ err, attempt: inboxFails - 1, intervalMs: inboxIntervalMs, random });
|
|
3101
|
+
await sleep(readLocalJobs(dir).some((job) => job.trigger) ? Math.min(retryDelay, inboxIntervalMs) : retryDelay);
|
|
3006
3102
|
continue;
|
|
3007
3103
|
}
|
|
3008
3104
|
if (stopping || state !== "running")
|
package/dist/lib/bot/status.d.ts
CHANGED
|
@@ -85,8 +85,29 @@ export interface BotStatusFile {
|
|
|
85
85
|
} | null;
|
|
86
86
|
/** hq-cli version this process loaded (a bot keeps it until it restarts). */
|
|
87
87
|
cliVersion?: string;
|
|
88
|
+
/**
|
|
89
|
+
* HQ Fleet Runtime bots: the last turn that ended in the canned away line,
|
|
90
|
+
* as the bot reported it to HQ and to the owner (written by the HQ DM
|
|
91
|
+
* adapter in hq-fleet-local).
|
|
92
|
+
*/
|
|
93
|
+
lastFailure?: BotLastFailure;
|
|
88
94
|
updatedAt: string;
|
|
89
95
|
}
|
|
96
|
+
export interface BotLastFailure {
|
|
97
|
+
at: string;
|
|
98
|
+
/** auth | billing | rate_limit | outage | runtime_error | generic */
|
|
99
|
+
class: string;
|
|
100
|
+
site?: string;
|
|
101
|
+
provider?: string | null;
|
|
102
|
+
/** Plain-language cause, as the owner was told. */
|
|
103
|
+
cause: string;
|
|
104
|
+
/** Plain-language fix, as the owner was told. */
|
|
105
|
+
fix: string;
|
|
106
|
+
/** The HQ failure report id, when the upload succeeded. */
|
|
107
|
+
reportId?: string | null;
|
|
108
|
+
uploaded?: boolean;
|
|
109
|
+
ownerNotified?: boolean;
|
|
110
|
+
}
|
|
90
111
|
export declare function defaultBotStatus(pid: number, runtime: BotRuntimeId, now?: () => Date): BotStatusFile;
|
|
91
112
|
export declare function readBotStatus(dir: string): BotStatusFile | null;
|
|
92
113
|
export declare function writeBotStatus(dir: string, status: BotStatusFile): void;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type ClaudeLoginCheck } from "../../bot/claude-login.js";
|
|
2
|
+
import { type BotStatusFile } from "../../bot/status.js";
|
|
1
3
|
import { type NodeBinaryIo } from "../../node-binary.js";
|
|
2
4
|
import type { CheckContext, CheckFamily, CheckResult } from "../types.js";
|
|
3
5
|
export declare const BOTS_FAMILY_ID = "bots";
|
|
@@ -7,7 +9,11 @@ export interface BotsDoctorDeps {
|
|
|
7
9
|
root: (home?: string) => string;
|
|
8
10
|
/** Filesystem view used to judge the startup agent's node path (tests). */
|
|
9
11
|
nodeIo?: NodeBinaryIo;
|
|
12
|
+
/** The Claude Code login read the way the HQ Fleet Runtime reads it (tests). */
|
|
13
|
+
claudeLogin?: () => ClaudeLoginCheck;
|
|
10
14
|
}
|
|
15
|
+
/** The last reported failure, unless a later turn worked. */
|
|
16
|
+
export declare function unresolvedLastFailure(status: BotStatusFile | null): BotStatusFile["lastFailure"] | null;
|
|
11
17
|
export declare const botsFamily: CheckFamily;
|
|
12
18
|
export declare function checkBots(_context: CheckContext, deps?: BotsDoctorDeps): CheckResult[];
|
|
13
19
|
//# sourceMappingURL=bots.d.ts.map
|