@indigoai-us/hq-cli 5.301.0 → 5.303.0
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 +26 -0
- package/assets/scaffold/core/scripts/rebuild-projects-index.sh +99 -12
- package/assets/scaffold/core/scripts/token-usage-report.sh +72 -20
- package/dist/command-catalog.generated.d.ts +4 -1
- package/dist/command-catalog.generated.js +5 -1
- package/dist/commands/bot.js +1 -0
- package/dist/commands/core-checkpoint.js +1 -0
- package/dist/commands/core-handoff-fast.js +1 -0
- package/dist/commands/core-handoff.js +2 -0
- package/dist/commands/deploy-comments.js +5 -2
- package/dist/commands/integrations-connect.d.ts +3 -0
- package/dist/commands/integrations-connect.js +21 -2
- package/dist/commands/lanes-message.js +33 -3
- package/dist/commands/lanes-questions.d.ts +4 -2
- package/dist/commands/lanes-questions.js +123 -2
- package/dist/commands/lanes-resume.js +25 -4
- package/dist/commands/lanes-shared.d.ts +13 -1
- package/dist/commands/lanes-watch.js +5 -1
- package/dist/commands/lanes.js +91 -9
- package/dist/commands/monitor.js +17 -2
- package/dist/commands/report.js +5 -2
- package/dist/commands/rescue.d.ts +4 -0
- package/dist/commands/rescue.js +27 -17
- package/dist/lib/agent-kit/fallback.d.ts +1 -0
- package/dist/lib/agent-kit/fallback.js +1 -0
- package/dist/lib/bot/cli-update.d.ts +0 -8
- package/dist/lib/bot/cli-update.js +5 -5
- package/dist/lib/bot/daemon.d.ts +5 -1
- package/dist/lib/bot/daemon.js +4 -4
- package/dist/lib/bot/local-jobs.d.ts +1 -21
- package/dist/lib/bot/local-jobs.js +12 -12
- package/dist/lib/core-utils/soft-timeout.js +1 -1
- package/dist/lib/daemon/config.d.ts +0 -1
- package/dist/lib/daemon/config.js +1 -1
- package/dist/lib/deploy-apps.d.ts +13 -0
- package/dist/lib/deploy-apps.js +24 -0
- package/dist/lib/index-render/projects.js +82 -5
- package/dist/lib/lanes/delivery.d.ts +7 -3
- package/dist/lib/lanes/delivery.js +18 -8
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.d.ts +13 -3
- package/dist/lib/lanes/keep-alive.js +29 -5
- package/dist/lib/lanes/outpost-dispatch.d.ts +5 -0
- package/dist/lib/lanes/outpost-dispatch.js +12 -0
- package/dist/lib/lanes/reconcile.d.ts +1 -0
- package/dist/lib/lanes/reconcile.js +358 -38
- package/dist/lib/lanes/spawn.d.ts +1 -0
- package/dist/lib/lanes/spawn.js +20 -10
- package/dist/lib/lanes/store.js +24 -0
- package/dist/lib/lanes/story-ledger.d.ts +2 -0
- package/dist/lib/lanes/story-ledger.js +8 -0
- package/dist/lib/lanes/types.d.ts +26 -4
- package/dist/lib/lanes/types.js +33 -1
- package/dist/lib/lanes/watch.d.ts +1 -0
- package/dist/lib/lanes/watch.js +19 -6
- package/dist/lib/lanes/watcher.js +139 -9
- package/dist/lib/mesh/live/daemon/run.js +36 -2
- package/dist/lib/report-snapshot.d.ts +0 -2
- package/dist/lib/report-snapshot.js +2 -2
- package/dist/lib/search-index/background.d.ts +1 -0
- package/dist/lib/search-index/background.js +1 -0
- package/dist/utils/run-bundled-script.js +103 -1
- package/dist/utils/update-command-supervisor.cjs +1 -0
- package/dist/utils/version-gate.js +1 -1
- package/package.json +1 -1
package/dist/commands/rescue.js
CHANGED
|
@@ -53,38 +53,48 @@ function ghHeaders(token) {
|
|
|
53
53
|
headers.Authorization = `Bearer ${token}`;
|
|
54
54
|
return headers;
|
|
55
55
|
}
|
|
56
|
+
// Thirty seconds gives GitHub time to respond while keeping the recovery command bounded.
|
|
57
|
+
const GITHUB_REQUEST_TIMEOUT_MS = 30_000;
|
|
58
|
+
async function githubJsonRequest(url, token) {
|
|
59
|
+
const signal = AbortSignal.timeout(GITHUB_REQUEST_TIMEOUT_MS);
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch(url, { headers: ghHeaders(token), signal });
|
|
62
|
+
if (!response.ok)
|
|
63
|
+
return { response };
|
|
64
|
+
const body = (await response.json());
|
|
65
|
+
return { response, body };
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (signal.aborted)
|
|
69
|
+
throw new Error('GitHub request timed out', { cause: error });
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
56
73
|
/** Latest release tag for a repo, e.g. `v12.3.0`. Throws on failure. */
|
|
57
|
-
async function getLatestReleaseTag(repo, token) {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (!res.ok) {
|
|
62
|
-
throw new Error(`GitHub releases/latest for ${repo}: HTTP ${res.status}`);
|
|
74
|
+
export async function getLatestReleaseTag(repo, token) {
|
|
75
|
+
const { response, body } = await githubJsonRequest(`https://api.github.com/repos/${repo}/releases/latest`, token);
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
throw new Error(`GitHub releases/latest for ${repo}: HTTP ${response.status}`);
|
|
63
78
|
}
|
|
64
|
-
|
|
65
|
-
if (!body.tag_name)
|
|
79
|
+
if (!body?.tag_name)
|
|
66
80
|
throw new Error(`no tag_name in latest release for ${repo}`);
|
|
67
81
|
return body.tag_name;
|
|
68
82
|
}
|
|
69
83
|
/** Resolve a tag to its commit SHA, dereferencing annotated tags. */
|
|
70
|
-
async function resolveTagSha(repo, tag, token) {
|
|
71
|
-
const refRes = await
|
|
84
|
+
export async function resolveTagSha(repo, tag, token) {
|
|
85
|
+
const { response: refRes, body: ref } = await githubJsonRequest(`https://api.github.com/repos/${repo}/git/ref/tags/${encodeURIComponent(tag)}`, token);
|
|
72
86
|
if (!refRes.ok)
|
|
73
87
|
throw new Error(`git/ref/tags/${tag} for ${repo}: HTTP ${refRes.status}`);
|
|
74
|
-
const
|
|
75
|
-
const obj = ref.object;
|
|
88
|
+
const obj = ref?.object;
|
|
76
89
|
if (!obj?.sha)
|
|
77
90
|
throw new Error(`no object.sha for tag ${tag} in ${repo}`);
|
|
78
91
|
if (obj.type !== 'tag')
|
|
79
92
|
return obj.sha; // lightweight tag → already a commit
|
|
80
93
|
// Annotated tag → dereference to the commit it points at.
|
|
81
|
-
const tagRes = await
|
|
82
|
-
headers: ghHeaders(token),
|
|
83
|
-
});
|
|
94
|
+
const { response: tagRes, body: annotated } = await githubJsonRequest(`https://api.github.com/repos/${repo}/git/tags/${obj.sha}`, token);
|
|
84
95
|
if (!tagRes.ok)
|
|
85
96
|
throw new Error(`git/tags/${obj.sha} for ${repo}: HTTP ${tagRes.status}`);
|
|
86
|
-
|
|
87
|
-
if (!annotated.object?.sha)
|
|
97
|
+
if (!annotated?.object?.sha)
|
|
88
98
|
throw new Error(`annotated tag ${tag} has no target sha`);
|
|
89
99
|
return annotated.object.sha;
|
|
90
100
|
}
|
|
@@ -37,6 +37,7 @@ export declare function stopFallback(paths: Pick<AgentKitPaths, "agentDir">, kil
|
|
|
37
37
|
export interface DetachedSpawn {
|
|
38
38
|
(cmd: string, args: string[], opts: {
|
|
39
39
|
detached: true;
|
|
40
|
+
windowsHide: true;
|
|
40
41
|
stdio: ["ignore", number, number];
|
|
41
42
|
env: NodeJS.ProcessEnv;
|
|
42
43
|
cwd: string;
|
|
@@ -113,6 +113,7 @@ export function startFallback(paths, host, deps = {}) {
|
|
|
113
113
|
try {
|
|
114
114
|
const child = doSpawn(cmd, args, {
|
|
115
115
|
detached: true,
|
|
116
|
+
windowsHide: true,
|
|
116
117
|
stdio: ["ignore", fd, fd],
|
|
117
118
|
env: { ...(deps.env ?? process.env), HOME: host.home, ...kitServiceEnv(paths) },
|
|
118
119
|
cwd: paths.agentDir,
|
|
@@ -35,16 +35,8 @@ import type { BotLogger } from "./log.js";
|
|
|
35
35
|
export declare const CLI_UPDATE_RESTART_EXIT_CODE = 75;
|
|
36
36
|
/** How often the installed package.json is read. */
|
|
37
37
|
export declare const CLI_UPDATE_CHECK_INTERVAL_MS = 60000;
|
|
38
|
-
/** Consecutive checks that must agree on the new version before acting. */
|
|
39
|
-
export declare const CLI_UPDATE_CONFIRMATIONS = 2;
|
|
40
|
-
/** At most this many update restarts per window, whatever the versions. */
|
|
41
|
-
export declare const CLI_UPDATE_RESTART_LIMIT = 4;
|
|
42
|
-
export declare const CLI_UPDATE_RESTART_WINDOW_MS: number;
|
|
43
|
-
/** A restart that came back on the old version blocks that target this long. */
|
|
44
|
-
export declare const CLI_UPDATE_NOT_CONVERGED_COOLDOWN_MS: number;
|
|
45
38
|
/** Set to "1" to turn the watcher off (development runs). */
|
|
46
39
|
export declare const CLI_UPDATE_DISABLE_ENV = "HQ_BOT_NO_CLI_UPDATE_RESTART";
|
|
47
|
-
export declare const CLI_UPDATE_LEDGER_FILE = "cli-update.json";
|
|
48
40
|
export type InstalledCliRead = {
|
|
49
41
|
ok: true;
|
|
50
42
|
version: string;
|
|
@@ -42,15 +42,15 @@ export const CLI_UPDATE_RESTART_EXIT_CODE = 75;
|
|
|
42
42
|
/** How often the installed package.json is read. */
|
|
43
43
|
export const CLI_UPDATE_CHECK_INTERVAL_MS = 60_000;
|
|
44
44
|
/** Consecutive checks that must agree on the new version before acting. */
|
|
45
|
-
|
|
45
|
+
const CLI_UPDATE_CONFIRMATIONS = 2;
|
|
46
46
|
/** At most this many update restarts per window, whatever the versions. */
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
const CLI_UPDATE_RESTART_LIMIT = 4;
|
|
48
|
+
const CLI_UPDATE_RESTART_WINDOW_MS = 60 * 60_000;
|
|
49
49
|
/** A restart that came back on the old version blocks that target this long. */
|
|
50
|
-
|
|
50
|
+
const CLI_UPDATE_NOT_CONVERGED_COOLDOWN_MS = 6 * 60 * 60_000;
|
|
51
51
|
/** Set to "1" to turn the watcher off (development runs). */
|
|
52
52
|
export const CLI_UPDATE_DISABLE_ENV = "HQ_BOT_NO_CLI_UPDATE_RESTART";
|
|
53
|
-
|
|
53
|
+
const CLI_UPDATE_LEDGER_FILE = "cli-update.json";
|
|
54
54
|
/**
|
|
55
55
|
* The hq-cli version installed behind `entry`, resolved the way the next launch
|
|
56
56
|
* of the same command line will resolve it: follow the symlinks now (a pnpm or
|
package/dist/lib/bot/daemon.d.ts
CHANGED
|
@@ -78,7 +78,11 @@ export interface BotDaemonDeps {
|
|
|
78
78
|
*/
|
|
79
79
|
bootout?: "inline" | "detached";
|
|
80
80
|
/** Injectable detached spawn, so tests never reach the real service manager. */
|
|
81
|
-
spawnDetached?: (command: string, args: string[]
|
|
81
|
+
spawnDetached?: (command: string, args: string[], options: {
|
|
82
|
+
detached: true;
|
|
83
|
+
windowsHide: true;
|
|
84
|
+
stdio: "ignore";
|
|
85
|
+
}) => void;
|
|
82
86
|
/**
|
|
83
87
|
* Called once the removal is durable (plist gone) but before the bootout —
|
|
84
88
|
* the last moment a self-booting-out caller is still alive to say anything.
|
package/dist/lib/bot/daemon.js
CHANGED
|
@@ -149,8 +149,8 @@ function shQuote(value) {
|
|
|
149
149
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
150
150
|
}
|
|
151
151
|
/** Detached `launchctl bootout` that outlives the process asking for it. */
|
|
152
|
-
function defaultSpawnDetached(command, args) {
|
|
153
|
-
const child = spawn(command, args,
|
|
152
|
+
function defaultSpawnDetached(command, args, options) {
|
|
153
|
+
const child = spawn(command, args, options);
|
|
154
154
|
child.unref();
|
|
155
155
|
}
|
|
156
156
|
/**
|
|
@@ -165,7 +165,7 @@ function defaultSpawnDetached(command, args) {
|
|
|
165
165
|
function bootoutLaunchd(p, deps) {
|
|
166
166
|
const target = `${launchdDomain(deps.uid)}/${p.label}`;
|
|
167
167
|
if (deps.bootout === "detached") {
|
|
168
|
-
(deps.spawnDetached ?? defaultSpawnDetached)("/bin/sh", ["-c", `sleep 1; exec launchctl bootout ${shQuote(target)}`]);
|
|
168
|
+
(deps.spawnDetached ?? defaultSpawnDetached)("/bin/sh", ["-c", `sleep 1; exec launchctl bootout ${shQuote(target)}`], { detached: true, windowsHide: true, stdio: "ignore" });
|
|
169
169
|
return;
|
|
170
170
|
}
|
|
171
171
|
deps.launchctl?.(["bootout", target]);
|
|
@@ -336,7 +336,7 @@ export function healBotDaemonNodeFromJob(p, deps = {}) {
|
|
|
336
336
|
`for d in 0 1 2 4; do sleep $d; launchctl bootstrap ${shQuote(domain)} ${shQuote(p.plistPath)} >/dev/null 2>&1 && exit 0; done`,
|
|
337
337
|
"exit 1",
|
|
338
338
|
].join("; ");
|
|
339
|
-
(deps.spawnDetached ?? defaultSpawnDetached)("/bin/sh", ["-c", script]);
|
|
339
|
+
(deps.spawnDetached ?? defaultSpawnDetached)("/bin/sh", ["-c", script], { detached: true, windowsHide: true, stdio: "ignore" });
|
|
340
340
|
return fileNeedsRewrite ? { from: file.nodeBinary, to: fileTarget } : { from: loaded, to: loadedTarget };
|
|
341
341
|
}
|
|
342
342
|
/** `program = …` of the loaded job (`launchctl print`), or null when not loaded or unknown. */
|
|
@@ -18,9 +18,7 @@
|
|
|
18
18
|
* - A fire is recorded before it is handed over, so a crash never runs the
|
|
19
19
|
* same due time twice.
|
|
20
20
|
*/
|
|
21
|
-
import { type BotApi, type
|
|
22
|
-
export declare const LOCAL_JOBS_FILE = "jobs.json";
|
|
23
|
-
export declare const LOCAL_JOB_MESSAGE_PREFIX = "localjob-";
|
|
21
|
+
import { type BotApi, type InboxItem } from "./api.js";
|
|
24
22
|
export interface LocalJob {
|
|
25
23
|
jobId: string;
|
|
26
24
|
prompt: string;
|
|
@@ -40,9 +38,7 @@ export interface LocalJob {
|
|
|
40
38
|
lastStatus?: string;
|
|
41
39
|
runCount?: number;
|
|
42
40
|
}
|
|
43
|
-
export declare function localJobsPath(dir: string): string;
|
|
44
41
|
export declare function readLocalJobs(dir: string): LocalJob[];
|
|
45
|
-
export declare function writeLocalJobs(dir: string, jobs: LocalJob[]): void;
|
|
46
42
|
/**
|
|
47
43
|
* Run a read-change-write of `jobs.json` under an exclusive lock. The running
|
|
48
44
|
* bot (firing, finishing) and `hq bot jobs` (add, remove) are separate
|
|
@@ -67,40 +63,24 @@ export interface CronMatcher {
|
|
|
67
63
|
export declare function toStandardCron(expr: string): string | null;
|
|
68
64
|
/** Parse a standard 5-field cron, or null when it is not one. */
|
|
69
65
|
export declare function parseCron(expr: string): CronMatcher | null;
|
|
70
|
-
export declare function cronMatches(cron: CronMatcher, date: Date, timeZone: string): boolean;
|
|
71
|
-
/**
|
|
72
|
-
* The newest time this job was due, at or before `now` and after the last one
|
|
73
|
-
* already fired. Null when nothing new is due.
|
|
74
|
-
*/
|
|
75
|
-
/** `rate(5 minutes)` / `rate(1 hour)` / `rate(2 days)` as milliseconds. */
|
|
76
|
-
export declare function rateIntervalMs(expr: string): number | null;
|
|
77
|
-
export declare function isRunnableSchedule(expr: string): boolean;
|
|
78
66
|
export declare function latestDueFire(job: LocalJob, now: Date): Date | null;
|
|
79
67
|
/** The next time this job is due after `now`, within a week; null otherwise. */
|
|
80
68
|
export declare function nextDueFire(job: LocalJob, now: Date): Date | null;
|
|
81
|
-
export declare function toJobRecord(job: LocalJob, now?: Date): BotJobRecord;
|
|
82
69
|
export declare function addLocalJob(dir: string, input: {
|
|
83
70
|
prompt: string;
|
|
84
71
|
cron: string;
|
|
85
72
|
timezone: string;
|
|
86
73
|
context?: Record<string, unknown>;
|
|
87
74
|
}, now?: Date): LocalJob;
|
|
88
|
-
/** Remove a job. Returns false when the bot has no job with that id. */
|
|
89
|
-
export declare function removeLocalJob(dir: string, jobId: string): boolean;
|
|
90
75
|
/** Forget the once-per-minute check (tests, and a bot restarted in-process). */
|
|
91
76
|
export declare function resetLocalJobChecks(): void;
|
|
92
77
|
export declare function takeDueLocalJobRuns(dir: string, now?: Date): InboxItem[];
|
|
93
|
-
export declare function isLocalJobMessage(messageId: string): boolean;
|
|
94
|
-
/** Record how a local run finished and release the job for its next fire. */
|
|
95
|
-
export declare function finishLocalJobRun(dir: string, jobId: string, messageId: string, completion: Pick<JobRunCompletion, "status">, now?: Date): void;
|
|
96
78
|
/**
|
|
97
79
|
* The bot's cloud client, with its scheduled runs answered on this computer:
|
|
98
80
|
* due local jobs join the inbox, and their ack and completion are recorded
|
|
99
81
|
* locally instead of being sent to the cloud.
|
|
100
82
|
*/
|
|
101
83
|
export declare function withLocalJobs(api: BotApi, dir: string, clock?: () => Date): BotApi;
|
|
102
|
-
/** The most jobs one bot may hold (the same cap the cloud uses). */
|
|
103
|
-
export declare const MAX_LOCAL_JOBS = 25;
|
|
104
84
|
/**
|
|
105
85
|
* `hq bot jobs` against the bot's own folder: the same three calls the cloud
|
|
106
86
|
* client offers, answered from `jobs.json`.
|
|
@@ -22,15 +22,15 @@ 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
|
-
|
|
26
|
-
|
|
25
|
+
const LOCAL_JOBS_FILE = "jobs.json";
|
|
26
|
+
const LOCAL_JOB_MESSAGE_PREFIX = "localjob-";
|
|
27
27
|
/** How far back a missed fire is still looked for (one week). */
|
|
28
28
|
const LOOKBACK_MINUTES = 7 * 24 * 60;
|
|
29
29
|
/** A run that never reported back releases its job after this long. */
|
|
30
30
|
const IN_FLIGHT_RELEASE_MS = 60 * 60 * 1000;
|
|
31
31
|
/** A fire is "late" (a catch-up) once this long past its due time. */
|
|
32
32
|
const STALE_AFTER_MS = 10 * 60 * 1000;
|
|
33
|
-
|
|
33
|
+
function localJobsPath(dir) {
|
|
34
34
|
return path.join(dir, LOCAL_JOBS_FILE);
|
|
35
35
|
}
|
|
36
36
|
export function readLocalJobs(dir) {
|
|
@@ -42,7 +42,7 @@ export function readLocalJobs(dir) {
|
|
|
42
42
|
return [];
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
function writeLocalJobs(dir, jobs) {
|
|
46
46
|
const file = localJobsPath(dir);
|
|
47
47
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
48
48
|
fs.writeFileSync(tmp, JSON.stringify({ v: 1, jobs }, null, 2));
|
|
@@ -210,7 +210,7 @@ function zonedParts(date, timeZone) {
|
|
|
210
210
|
weekday: WEEKDAY[get("weekday")] ?? -1,
|
|
211
211
|
};
|
|
212
212
|
}
|
|
213
|
-
|
|
213
|
+
function cronMatches(cron, date, timeZone) {
|
|
214
214
|
const t = zonedParts(date, timeZone);
|
|
215
215
|
if (!cron.minutes.has(t.minute) || !cron.hours.has(t.hour) || !cron.months.has(t.month))
|
|
216
216
|
return false;
|
|
@@ -226,7 +226,7 @@ export function cronMatches(cron, date, timeZone) {
|
|
|
226
226
|
* already fired. Null when nothing new is due.
|
|
227
227
|
*/
|
|
228
228
|
/** `rate(5 minutes)` / `rate(1 hour)` / `rate(2 days)` as milliseconds. */
|
|
229
|
-
|
|
229
|
+
function rateIntervalMs(expr) {
|
|
230
230
|
const match = /^rate\((\d+)\s+(minute|minutes|hour|hours|day|days)\)$/.exec(expr.trim());
|
|
231
231
|
if (!match)
|
|
232
232
|
return null;
|
|
@@ -234,7 +234,7 @@ export function rateIntervalMs(expr) {
|
|
|
234
234
|
const unit = match[2].startsWith("minute") ? 60_000 : match[2].startsWith("hour") ? 3_600_000 : 86_400_000;
|
|
235
235
|
return n > 0 ? n * unit : null;
|
|
236
236
|
}
|
|
237
|
-
|
|
237
|
+
function isRunnableSchedule(expr) {
|
|
238
238
|
return rateIntervalMs(expr) !== null || parseCron(expr) !== null;
|
|
239
239
|
}
|
|
240
240
|
export function latestDueFire(job, now) {
|
|
@@ -281,7 +281,7 @@ export function nextDueFire(job, now) {
|
|
|
281
281
|
return null;
|
|
282
282
|
}
|
|
283
283
|
// ─── The job list (what `hq bot jobs` reads and writes) ──────────────────────
|
|
284
|
-
|
|
284
|
+
function toJobRecord(job, now = new Date()) {
|
|
285
285
|
const next = nextDueFire(job, now);
|
|
286
286
|
return {
|
|
287
287
|
jobId: job.jobId,
|
|
@@ -311,7 +311,7 @@ export function addLocalJob(dir, input, now = new Date()) {
|
|
|
311
311
|
return job;
|
|
312
312
|
}
|
|
313
313
|
/** Remove a job. Returns false when the bot has no job with that id. */
|
|
314
|
-
|
|
314
|
+
function removeLocalJob(dir, jobId) {
|
|
315
315
|
return withJobsLock(dir, () => {
|
|
316
316
|
const jobs = readLocalJobs(dir);
|
|
317
317
|
const next = jobs.filter((job) => job.jobId !== jobId);
|
|
@@ -375,11 +375,11 @@ export function takeDueLocalJobRuns(dir, now = new Date()) {
|
|
|
375
375
|
return items;
|
|
376
376
|
});
|
|
377
377
|
}
|
|
378
|
-
|
|
378
|
+
function isLocalJobMessage(messageId) {
|
|
379
379
|
return messageId.startsWith(LOCAL_JOB_MESSAGE_PREFIX);
|
|
380
380
|
}
|
|
381
381
|
/** Record how a local run finished and release the job for its next fire. */
|
|
382
|
-
|
|
382
|
+
function finishLocalJobRun(dir, jobId, messageId, completion, now = new Date()) {
|
|
383
383
|
withJobsLock(dir, () => {
|
|
384
384
|
const jobs = readLocalJobs(dir);
|
|
385
385
|
let changed = false;
|
|
@@ -428,7 +428,7 @@ export function withLocalJobs(api, dir, clock = () => new Date()) {
|
|
|
428
428
|
return local;
|
|
429
429
|
}
|
|
430
430
|
/** The most jobs one bot may hold (the same cap the cloud uses). */
|
|
431
|
-
|
|
431
|
+
const MAX_LOCAL_JOBS = 25;
|
|
432
432
|
/**
|
|
433
433
|
* `hq bot jobs` against the bot's own folder: the same three calls the cloud
|
|
434
434
|
* client offers, answered from `jobs.json`.
|
|
@@ -110,7 +110,7 @@ export function runSoftTimeout(parsed, deps = {}) {
|
|
|
110
110
|
// orphaned grandchild (e.g. run-project's builder) keeps running after the
|
|
111
111
|
// wrapper returns. stdio inherited so the command's stdin/stdout/stderr —
|
|
112
112
|
// including piped or interactive input — pass through unchanged.
|
|
113
|
-
const child = spawnFn(command[0], command.slice(1), { stdio: "inherit", detached: true });
|
|
113
|
+
const child = spawnFn(command[0], command.slice(1), { stdio: "inherit", detached: true, windowsHide: true });
|
|
114
114
|
let capped = false;
|
|
115
115
|
let ticks = 0;
|
|
116
116
|
// Signal the child's process group when we can (detached leader), else the
|
|
@@ -23,7 +23,6 @@ export interface DaemonConfig {
|
|
|
23
23
|
}
|
|
24
24
|
export type SyncOwner = "daemon" | "desktop-app";
|
|
25
25
|
export declare function effectiveSyncOwner(config: Pick<DaemonConfig, "syncOwner">, machineType: MachineType): SyncOwner;
|
|
26
|
-
export declare function emptyDaemonConfig(): DaemonConfig;
|
|
27
26
|
export declare function readDaemonConfig(file: string): DaemonConfig;
|
|
28
27
|
export declare function writeDaemonConfig(file: string, config: DaemonConfig): void;
|
|
29
28
|
/**
|
|
@@ -13,7 +13,7 @@ import { ensurePrivateDir } from "./paths.js";
|
|
|
13
13
|
export function effectiveSyncOwner(config, machineType) {
|
|
14
14
|
return config.syncOwner ?? (machineType === "desktop" ? "desktop-app" : "daemon");
|
|
15
15
|
}
|
|
16
|
-
|
|
16
|
+
function emptyDaemonConfig() {
|
|
17
17
|
return { version: 1, syncPaused: false, enable: [], disable: [] };
|
|
18
18
|
}
|
|
19
19
|
function stringList(value) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Pagination for org-scoped hq-deploy app listings. */
|
|
2
|
+
export declare const MAX_DEPLOY_APPS_PAGES = 100;
|
|
3
|
+
export interface DeployAppsPage<T> {
|
|
4
|
+
apps?: unknown;
|
|
5
|
+
nextCursor?: unknown;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Collect every page from GET /api/apps. Older hq-deploy versions omit
|
|
9
|
+
* nextCursor, which remains a valid single-page response. The cap prevents a
|
|
10
|
+
* bad cursor response from creating an unbounded CLI request loop.
|
|
11
|
+
*/
|
|
12
|
+
export declare function listDeployApps<T>(fetchPage: (cursor?: string) => Promise<DeployAppsPage<T>>): Promise<T[]>;
|
|
13
|
+
//# sourceMappingURL=deploy-apps.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Pagination for org-scoped hq-deploy app listings. */
|
|
2
|
+
export const MAX_DEPLOY_APPS_PAGES = 100;
|
|
3
|
+
/**
|
|
4
|
+
* Collect every page from GET /api/apps. Older hq-deploy versions omit
|
|
5
|
+
* nextCursor, which remains a valid single-page response. The cap prevents a
|
|
6
|
+
* bad cursor response from creating an unbounded CLI request loop.
|
|
7
|
+
*/
|
|
8
|
+
export async function listDeployApps(fetchPage) {
|
|
9
|
+
const apps = [];
|
|
10
|
+
let cursor;
|
|
11
|
+
for (let page = 0; page < MAX_DEPLOY_APPS_PAGES; page += 1) {
|
|
12
|
+
const result = await fetchPage(cursor);
|
|
13
|
+
if (Array.isArray(result.apps))
|
|
14
|
+
apps.push(...result.apps);
|
|
15
|
+
if (result.nextCursor === undefined || result.nextCursor === null)
|
|
16
|
+
return apps;
|
|
17
|
+
if (typeof result.nextCursor !== "string" || result.nextCursor.length === 0) {
|
|
18
|
+
throw new Error("hq-deploy returned an invalid apps-list cursor.");
|
|
19
|
+
}
|
|
20
|
+
cursor = result.nextCursor;
|
|
21
|
+
}
|
|
22
|
+
throw new Error(`hq-deploy apps list exceeded the ${MAX_DEPLOY_APPS_PAGES}-page safety cap.`);
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=deploy-apps.js.map
|
|
@@ -1,16 +1,92 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
1
2
|
import * as path from "path";
|
|
2
|
-
import { basename, date, exists, immediateEntries, isHidden, log, projectStatus, readJson, sanitize, truncate, write } from "./shared.js";
|
|
3
|
+
import { at, basename, date, exists, extractKeepFences, generatedFingerprint, generatedStamp, hasUnclosedKeepMarker, immediateEntries, isHidden, log, looksAutoGenerated, projectStatus, readJson, readText, sanitize, stampGenerated, titleize, truncate, write, } from "./shared.js";
|
|
4
|
+
function composeCompanyProjectsIndex(raw, existing) {
|
|
5
|
+
const stamped = stampGenerated(raw);
|
|
6
|
+
const fences = existing === undefined ? "" : extractKeepFences(existing);
|
|
7
|
+
if (!fences)
|
|
8
|
+
return stamped;
|
|
9
|
+
return `${stamped.replace(/\n+$/, "\n")}\n${fences}\n`;
|
|
10
|
+
}
|
|
11
|
+
function shouldOverwriteCompanyProjectsIndex(context, existing, next) {
|
|
12
|
+
if (existing === undefined)
|
|
13
|
+
return "write";
|
|
14
|
+
if (context.force)
|
|
15
|
+
return "write";
|
|
16
|
+
if (hasUnclosedKeepMarker(existing))
|
|
17
|
+
return "skip";
|
|
18
|
+
const stored = generatedStamp(existing);
|
|
19
|
+
if (stored && stored === generatedFingerprint(existing))
|
|
20
|
+
return "write";
|
|
21
|
+
if (stored)
|
|
22
|
+
return "refuse";
|
|
23
|
+
if (looksAutoGenerated(existing))
|
|
24
|
+
return "write";
|
|
25
|
+
if (generatedFingerprint(existing) === generatedFingerprint(next))
|
|
26
|
+
return "write";
|
|
27
|
+
return "refuse";
|
|
28
|
+
}
|
|
29
|
+
function renderCompanyProjects(context, company, projectRoot) {
|
|
30
|
+
const written = [];
|
|
31
|
+
const lines = [`# ${titleize(company)} Projects`, "", `> Auto-generated. Updated: ${date(context.now)}`, "", "| Project | Status | Description |", "|---------|--------|-------------|"];
|
|
32
|
+
for (const directory of immediateEntries(context.root, projectRoot, "dir")) {
|
|
33
|
+
const name = basename(directory);
|
|
34
|
+
if (name.startsWith("_") || isHidden(name))
|
|
35
|
+
continue;
|
|
36
|
+
const prd = path.join(directory, "prd.json");
|
|
37
|
+
const parsed = readJson(prd);
|
|
38
|
+
const description = truncate(sanitize(typeof parsed?.description === "string" ? parsed.description : ""), 110) || "—";
|
|
39
|
+
lines.push(`| \`${name}\` | ${projectStatus(context.root, name, prd, "active", prd)} | ${description} |`);
|
|
40
|
+
}
|
|
41
|
+
lines.push("");
|
|
42
|
+
const output = `${projectRoot}/INDEX.md`;
|
|
43
|
+
const existing = readText(at(context.root, output));
|
|
44
|
+
// An unreadable existing file is not an invitation to replace it.
|
|
45
|
+
if (existing === undefined && fs.existsSync(at(context.root, output))) {
|
|
46
|
+
context.refused?.push(output);
|
|
47
|
+
log(context, `rebuild-projects-index: refused to overwrite unreadable ${output}`);
|
|
48
|
+
return written;
|
|
49
|
+
}
|
|
50
|
+
const next = composeCompanyProjectsIndex(lines.join("\n"), existing);
|
|
51
|
+
const decision = shouldOverwriteCompanyProjectsIndex(context, existing, next);
|
|
52
|
+
if (decision === "skip") {
|
|
53
|
+
log(context, `rebuild-projects-index: skipped ${output} (<!-- hq:keep -->)`);
|
|
54
|
+
return written;
|
|
55
|
+
}
|
|
56
|
+
if (decision === "refuse") {
|
|
57
|
+
context.refused?.push(output);
|
|
58
|
+
log(context, `rebuild-projects-index: refused ${output} (hand-written or edited; differs from last generated output)`);
|
|
59
|
+
log(context, "Re-run with --accept to overwrite, or add <!-- hq:keep --> to opt out.");
|
|
60
|
+
return written;
|
|
61
|
+
}
|
|
62
|
+
write(context, output, next);
|
|
63
|
+
written.push(output);
|
|
64
|
+
log(context, `rebuild-projects-index: wrote ${output}`);
|
|
65
|
+
return written;
|
|
66
|
+
}
|
|
3
67
|
export function renderProjects(context) {
|
|
68
|
+
const written = [];
|
|
69
|
+
const companyDirectories = immediateEntries(context.root, "companies", "dir");
|
|
70
|
+
for (const companyDirectory of companyDirectories) {
|
|
71
|
+
const company = basename(companyDirectory);
|
|
72
|
+
if (company.startsWith("_") || isHidden(company))
|
|
73
|
+
continue;
|
|
74
|
+
const projectRoot = ["companies", company, "projects"].join("/");
|
|
75
|
+
if (immediateEntries(context.root, projectRoot, "dir").length > 0 || fs.existsSync(at(context.root, projectRoot))) {
|
|
76
|
+
written.push(...renderCompanyProjects(context, company, projectRoot));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
4
79
|
const base = "personal/projects";
|
|
5
80
|
if (!exists(context.root, base)) {
|
|
6
81
|
log(context, "rebuild-projects-index: personal/projects/ missing, skipping");
|
|
7
|
-
return { written
|
|
82
|
+
return { written };
|
|
8
83
|
}
|
|
9
84
|
const lines = ["# Projects", "", `> Auto-generated. Updated: ${date(context.now)}`, "", "Personal/HQ projects only. Company-scoped projects live in `companies/{co}/projects/`.", "", "## Company Project Directories", "", "| Company | Path |", "|---------|------|"];
|
|
10
|
-
for (const companyDirectory of
|
|
85
|
+
for (const companyDirectory of companyDirectories) {
|
|
11
86
|
const company = basename(companyDirectory);
|
|
12
|
-
if (!company.startsWith("_") && !isHidden(company) &&
|
|
87
|
+
if (!company.startsWith("_") && !isHidden(company) && fs.existsSync(at(context.root, ["companies", company, "projects"].join("/")))) {
|
|
13
88
|
lines.push(`| ${company} | \`companies/${company}/projects/\` |`);
|
|
89
|
+
}
|
|
14
90
|
}
|
|
15
91
|
lines.push("", "## Personal/HQ Projects", "", "Projects here are HQ infrastructure or cross-company tools.", "", "| Project | Stories | Status | Description |", "|---------|---------|--------|-------------|");
|
|
16
92
|
const projects = immediateEntries(context.root, base, "dir");
|
|
@@ -27,7 +103,8 @@ export function renderProjects(context) {
|
|
|
27
103
|
lines.push("");
|
|
28
104
|
const output = `${base}/INDEX.md`;
|
|
29
105
|
write(context, output, lines.join("\n"));
|
|
106
|
+
written.push(output);
|
|
30
107
|
log(context, `rebuild-projects-index: wrote ${output} (${projects.length} HQ project(s))`);
|
|
31
|
-
return { written
|
|
108
|
+
return { written };
|
|
32
109
|
}
|
|
33
110
|
//# sourceMappingURL=projects.js.map
|
|
@@ -41,8 +41,8 @@ export declare function sessionNotPortableMessage(provider: LaneProvider): strin
|
|
|
41
41
|
* is not terminal (spec §6.7) and nothing stopped the lane deliberately.
|
|
42
42
|
*
|
|
43
43
|
* Refused by `hq lanes message`:
|
|
44
|
-
* `awaiting_input` has its own path, `hq lanes questions answer`,
|
|
45
|
-
*
|
|
44
|
+
* `awaiting_input` has its own path, `hq lanes questions answer`, while a
|
|
45
|
+
* senior may resume it when no answerable pending question remains. `killed` and
|
|
46
46
|
* `cancelled` were stopped on purpose by the operator; a message must not
|
|
47
47
|
* silently restart them. `failed` means the run ended without an envelope or
|
|
48
48
|
* never started; only the lane's senior can explicitly recover it with
|
|
@@ -66,6 +66,8 @@ export type ResumeCommandState = (typeof RESUME_COMMAND_STATES)[number];
|
|
|
66
66
|
export declare function isResumeCommandState(state: LaneState): state is ResumeCommandState;
|
|
67
67
|
export declare const LANE_DELIVERY_MECHANISMS: readonly ["dropbox", "resume", "none"];
|
|
68
68
|
export type LaneDeliveryMechanism = (typeof LANE_DELIVERY_MECHANISMS)[number];
|
|
69
|
+
/** Additional presentation-only result used by `hq lanes show`. */
|
|
70
|
+
export type LaneShowDeliveryMechanism = LaneDeliveryMechanism | "questions";
|
|
69
71
|
export type DeliveryLane = Pick<LaneRecord, "state" | "provider" | "provider_session_id" | "terminal_reason" | "envelope_repair">;
|
|
70
72
|
/** Only the terminal result of a failed automatic envelope repair is recoverable. */
|
|
71
73
|
export declare function isFailedEnvelopeRepairLane(lane: DeliveryLane): boolean;
|
|
@@ -139,7 +141,8 @@ export type LaunchTrigger = {
|
|
|
139
141
|
/** `hq lanes resume`: the operator's explicit opt-in to continue an exited lane. */
|
|
140
142
|
kind: "resume";
|
|
141
143
|
/** Lane state at the moment of the command; recorded as `last_resume.resumed_from`. */
|
|
142
|
-
from: ResumeCommandState | "failed";
|
|
144
|
+
from: ResumeCommandState | "failed" | "awaiting_input";
|
|
145
|
+
allowAwaitingInput?: true;
|
|
143
146
|
fromFailed?: true;
|
|
144
147
|
/** Senior that authorized a failed-lane resume, rechecked at the claim CAS. */
|
|
145
148
|
expectedSenior?: SeniorRef;
|
|
@@ -158,6 +161,7 @@ export type LaunchPlan = {
|
|
|
158
161
|
*/
|
|
159
162
|
export declare function resumeCommandRefusal(lane: DeliveryLane, options?: {
|
|
160
163
|
allowFailed?: boolean;
|
|
164
|
+
allowAwaitingInput?: boolean;
|
|
161
165
|
}): {
|
|
162
166
|
error: "lane_not_resumable" | "resume_unsupported" | "no_provider_session";
|
|
163
167
|
message: string;
|
|
@@ -61,8 +61,8 @@ export function sessionNotPortableMessage(provider) {
|
|
|
61
61
|
* is not terminal (spec §6.7) and nothing stopped the lane deliberately.
|
|
62
62
|
*
|
|
63
63
|
* Refused by `hq lanes message`:
|
|
64
|
-
* `awaiting_input` has its own path, `hq lanes questions answer`,
|
|
65
|
-
*
|
|
64
|
+
* `awaiting_input` has its own path, `hq lanes questions answer`, while a
|
|
65
|
+
* senior may resume it when no answerable pending question remains. `killed` and
|
|
66
66
|
* `cancelled` were stopped on purpose by the operator; a message must not
|
|
67
67
|
* silently restart them. `failed` means the run ended without an envelope or
|
|
68
68
|
* never started; only the lane's senior can explicitly recover it with
|
|
@@ -104,7 +104,7 @@ export function laneDeliveryMechanism(lane) {
|
|
|
104
104
|
if (lane.state === "queued" || lane.state === "running")
|
|
105
105
|
return "dropbox";
|
|
106
106
|
if (providerSupportsSessionResume(lane.provider) &&
|
|
107
|
-
(isResumableLaneState(lane.state) || isFailedEnvelopeRepairLane(lane)) &&
|
|
107
|
+
(isResumableLaneState(lane.state) || lane.state === "awaiting_input" || isFailedEnvelopeRepairLane(lane)) &&
|
|
108
108
|
hasRecordedSessionId(lane)) {
|
|
109
109
|
return "resume";
|
|
110
110
|
}
|
|
@@ -348,6 +348,7 @@ export const LAUNCH_TRIGGERS = ["admit", "answered_questions", "message", "resum
|
|
|
348
348
|
*/
|
|
349
349
|
export function resumeCommandRefusal(lane, options = {}) {
|
|
350
350
|
if (!isResumeCommandState(lane.state) &&
|
|
351
|
+
!(options.allowAwaitingInput === true && lane.state === "awaiting_input") &&
|
|
351
352
|
!isFailedEnvelopeRepairLane(lane) &&
|
|
352
353
|
!(options.allowFailed === true && lane.state === "failed")) {
|
|
353
354
|
return {
|
|
@@ -398,7 +399,10 @@ export function planLaneLaunch(lane, trigger) {
|
|
|
398
399
|
return { mode: "resume", session_id: lane.provider_session_id };
|
|
399
400
|
}
|
|
400
401
|
case "resume": {
|
|
401
|
-
const refusal = resumeCommandRefusal(lane, {
|
|
402
|
+
const refusal = resumeCommandRefusal(lane, {
|
|
403
|
+
allowFailed: trigger.fromFailed === true,
|
|
404
|
+
allowAwaitingInput: trigger.allowAwaitingInput === true,
|
|
405
|
+
});
|
|
402
406
|
if (refusal !== null || !hasRecordedSessionId(lane)) {
|
|
403
407
|
throw new LanesError(refusal?.error ?? "no_provider_session", refusal?.message ?? `Lane is ${lane.state} and has no recorded provider session`, { state: lane.state, provider: lane.provider });
|
|
404
408
|
}
|
|
@@ -419,11 +423,17 @@ export function claimableStatesFor(trigger, lane) {
|
|
|
419
423
|
case "answered_questions":
|
|
420
424
|
return ["awaiting_input"];
|
|
421
425
|
case "message":
|
|
422
|
-
return envelopeRepairFailed
|
|
423
|
-
|
|
424
|
-
|
|
426
|
+
return envelopeRepairFailed
|
|
427
|
+
? [...RESUMABLE_LANE_STATES, "failed"]
|
|
428
|
+
: lane?.state === "awaiting_input"
|
|
429
|
+
? [...RESUMABLE_LANE_STATES, "awaiting_input"]
|
|
430
|
+
: RESUMABLE_LANE_STATES;
|
|
431
|
+
case "resume": {
|
|
432
|
+
const states = trigger.fromFailed === true || envelopeRepairFailed
|
|
425
433
|
? [...RESUME_COMMAND_STATES, "failed"]
|
|
426
|
-
: RESUME_COMMAND_STATES;
|
|
434
|
+
: [...RESUME_COMMAND_STATES];
|
|
435
|
+
return lane?.state === "awaiting_input" ? [...states, "awaiting_input"] : states;
|
|
436
|
+
}
|
|
427
437
|
default: {
|
|
428
438
|
const _exhaustive = trigger;
|
|
429
439
|
return _exhaustive;
|
|
@@ -64,7 +64,7 @@ export type { RegistryWorkerEntry, ResolvedLaneWorker, WorkerTenancy, WorkerYaml
|
|
|
64
64
|
export { bodyStoreForLane, envelopeQuestionId, parseQuestionBodyStore, questionBodyAbsPath, questionBodyRef, raiseEnvelopeQuestions, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "./questions.js";
|
|
65
65
|
export type { QuestionBody, RaiseLaneQuestionInput } from "./questions.js";
|
|
66
66
|
export { LANE_DELIVERY_MECHANISMS, LAUNCH_TRIGGERS, PROVIDER_SESSION_RESUME, PROVIDER_SESSION_PORTABLE, providerSessionPortable, sessionNotPortableMessage, parseGrokResultOutput, RESUMABLE_LANE_STATES, RESUME_COMMAND_STATES, SESSION_ID_ABSENT_REASONS, claimableStatesFor, deliveryRefusalReason, hasRecordedSessionId, isResumableLaneState, isResumeCommandState, laneDeliveryMechanism, parseClaudeResultOutput, parseCodexEventOutput, parseWorkerRunOutput, planLaneLaunch, providerSupportsSessionResume, readWorkerRunResult, resumeCommandRefusal, } from "./delivery.js";
|
|
67
|
-
export type { DeliveryLane, LaneDeliveryMechanism, LaunchPlan, LaunchTrigger, LaunchTriggerKind, ResumableLaneState, ResumeCommandState, SessionIdAbsentReason, WorkerRunResult, } from "./delivery.js";
|
|
67
|
+
export type { DeliveryLane, LaneDeliveryMechanism, LaneShowDeliveryMechanism, LaunchPlan, LaunchTrigger, LaunchTriggerKind, ResumableLaneState, ResumeCommandState, SessionIdAbsentReason, WorkerRunResult, } from "./delivery.js";
|
|
68
68
|
export { DROPBOX_DIR, INBOX_NAME, appendDropboxMessage, answerDropboxMessage, drainDropbox, dropboxDir, dropboxInboxPath, formatOperatorInstruction, hookAdditionalContextJson, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneIdForAnswerDelivery, operatorMessageRefusalReason, readToolActivity, recordToolActivity, runDrainHook, toolActivityPath, TOOL_ACTIVITY_NAME, } from "./dropbox.js";
|
|
69
69
|
export type { DrainDeps, DrainHookResult, DropboxMessage, ToolActivityRecord, } from "./dropbox.js";
|
|
70
70
|
export { DRAIN_HOOK_BASENAME, DRAIN_HOOK_REGISTRATION_KINDS, DRAIN_HOOK_TIMEOUT_SECONDS, PROVIDER_REGISTRATION_KIND, drainHookCommand, drainHookRegistrationPath, drainHookShPath, installDrainHook, lanesHookAssetsDir, mergeClaudeSettingsHook, mergeCodexConfigHook, probeDrainHookRegistration, renderDrainHookJson, } from "./hooks-install.js";
|