@indigoai-us/hq-cli 5.301.0 → 5.302.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 +22 -0
- package/assets/scaffold/core/scripts/rebuild-projects-index.sh +99 -12
- 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/daemon.d.ts +5 -1
- package/dist/lib/bot/daemon.js +4 -4
- package/dist/lib/core-utils/soft-timeout.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/search-index/background.d.ts +1 -0
- package/dist/lib/search-index/background.js +1 -0
- package/dist/utils/update-command-supervisor.cjs +1 -0
- package/dist/utils/version-gate.js +1 -1
- package/package.json +1 -1
|
@@ -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";
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type LaneRecord, type ManagerStatusGateHistoryEntry, type QuestionRecord } from "./types.js";
|
|
2
2
|
export declare const LEAD_LANE_KEEP_ALIVE_FLAG = "lanes.lead-keep-alive";
|
|
3
3
|
export declare const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
|
|
4
|
+
export declare const LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_FLAG = "lanes.keep-alive-next-check-at";
|
|
5
|
+
export declare const LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_DEFAULT_ENABLED = false;
|
|
6
|
+
export declare const LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_MAX_DELAY_MS: number;
|
|
4
7
|
export declare const LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG = "lanes.keep-alive-resume-blocked";
|
|
5
8
|
export declare const LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_DEFAULT_ENABLED = false;
|
|
6
9
|
export declare const LEAD_LANE_KEEP_ALIVE_BLOCKED_GRACE_MS: number;
|
|
@@ -35,10 +38,17 @@ export type KeepAliveBlockedReason = NonNullable<LaneRecord["keep_alive_blocked_
|
|
|
35
38
|
export interface KeepAliveDoneEnvelope {
|
|
36
39
|
decision?: string;
|
|
37
40
|
story_complete?: boolean;
|
|
41
|
+
next_check_at?: string;
|
|
38
42
|
}
|
|
39
|
-
export type DoneKeepAliveVerdict = "eligible" | "standing_idle" | "story_complete";
|
|
43
|
+
export type DoneKeepAliveVerdict = "eligible" | "standing_idle" | "story_complete" | "next_check_at";
|
|
44
|
+
export declare function keepAliveNextCheckAtMs(value: unknown, nowMs: number, anchorMs?: number): number | undefined;
|
|
40
45
|
/** Shared done-state policy used by watch, reconcile, and the show command. */
|
|
41
|
-
export declare function doneKeepAliveVerdict(lane: Pick<LaneRecord, "keep_alive_standing">, envelope: KeepAliveDoneEnvelope, hasQueuedWork: boolean
|
|
46
|
+
export declare function doneKeepAliveVerdict(lane: Pick<LaneRecord, "keep_alive_standing">, envelope: KeepAliveDoneEnvelope, hasQueuedWork: boolean, options?: {
|
|
47
|
+
nextCheckAtEnabled?: boolean;
|
|
48
|
+
hasPendingWork?: boolean;
|
|
49
|
+
nowMs?: number;
|
|
50
|
+
nextCheckAnchorMs?: number;
|
|
51
|
+
}): DoneKeepAliveVerdict | null;
|
|
42
52
|
export declare function canResumeDoneKeepAliveLane(lane: Pick<LaneRecord, "keep_alive_standing">, envelope: KeepAliveDoneEnvelope, hasQueuedWork: boolean): boolean;
|
|
43
53
|
/** Work signals already persisted for a lane that ended its turn. */
|
|
44
54
|
export declare function hasPendingKeepAliveWork(hqRoot: string, lane: Pick<LaneRecord, "lane_id" | "company" | "pending_resume">, questionRecords?: readonly QuestionRecord[]): boolean;
|
|
@@ -3,8 +3,12 @@ import * as path from "node:path";
|
|
|
3
3
|
import { LanesError } from "./errors.js";
|
|
4
4
|
import { dropboxInboxHasMessages } from "./dropbox.js";
|
|
5
5
|
import { appendCappedHistory, listQuestionIds, readQuestion, retryUpdateLane } from "./store.js";
|
|
6
|
+
import { isIsoDateTime } from "./types.js";
|
|
6
7
|
export const LEAD_LANE_KEEP_ALIVE_FLAG = "lanes.lead-keep-alive";
|
|
7
8
|
export const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
|
|
9
|
+
export const LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_FLAG = "lanes.keep-alive-next-check-at";
|
|
10
|
+
export const LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_DEFAULT_ENABLED = false;
|
|
11
|
+
export const LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_MAX_DELAY_MS = 24 * 60 * 60 * 1000;
|
|
8
12
|
export const LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG = "lanes.keep-alive-resume-blocked";
|
|
9
13
|
export const LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_DEFAULT_ENABLED = false;
|
|
10
14
|
export const LEAD_LANE_KEEP_ALIVE_BLOCKED_GRACE_MS = 15 * 60 * 1000;
|
|
@@ -98,15 +102,35 @@ export function queueManagerStatusGateResume(hqRoot, laneId, entry, resumeText)
|
|
|
98
102
|
export const LEAD_LANE_KEEP_ALIVE_RESUME_TEXT = "Automatic keep-alive resume: your last turn ended with decision done and story_complete false. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
|
|
99
103
|
export const LEAD_LANE_KEEP_ALIVE_BLOCKED_RESUME_TEXT = "You ended blocked with no pending question. Record the event you wait on in waiting_on, re-check each blocker, and continue the next unblocked story.";
|
|
100
104
|
export const LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT = "Automatic keep-alive resume: your previous turn was interrupted by a provider process exit. Check your last state before continuing. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
|
|
105
|
+
export function keepAliveNextCheckAtMs(value, nowMs, anchorMs = nowMs) {
|
|
106
|
+
if (typeof value !== "string" || !isIsoDateTime(value))
|
|
107
|
+
return undefined;
|
|
108
|
+
const parsed = Date.parse(value);
|
|
109
|
+
if (parsed <= nowMs)
|
|
110
|
+
return undefined;
|
|
111
|
+
const stableAnchorMs = Number.isFinite(anchorMs) ? anchorMs : nowMs;
|
|
112
|
+
const capped = Math.min(parsed, stableAnchorMs + LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_MAX_DELAY_MS);
|
|
113
|
+
return capped > nowMs ? capped : undefined;
|
|
114
|
+
}
|
|
101
115
|
/** Shared done-state policy used by watch, reconcile, and the show command. */
|
|
102
|
-
export function doneKeepAliveVerdict(lane, envelope, hasQueuedWork) {
|
|
116
|
+
export function doneKeepAliveVerdict(lane, envelope, hasQueuedWork, options = {}) {
|
|
103
117
|
if (envelope.decision !== "done")
|
|
104
118
|
return null;
|
|
119
|
+
let verdict;
|
|
105
120
|
if (envelope.story_complete !== true)
|
|
106
|
-
|
|
107
|
-
if (lane.keep_alive_standing !== true)
|
|
108
|
-
|
|
109
|
-
|
|
121
|
+
verdict = "eligible";
|
|
122
|
+
else if (lane.keep_alive_standing !== true)
|
|
123
|
+
verdict = "story_complete";
|
|
124
|
+
else
|
|
125
|
+
verdict = hasQueuedWork ? "eligible" : "standing_idle";
|
|
126
|
+
if (verdict === "eligible" && options.nextCheckAtEnabled === true && options.hasPendingWork !== true) {
|
|
127
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
128
|
+
const anchorMs = options.nextCheckAnchorMs ?? nowMs;
|
|
129
|
+
const nextCheckAtMs = keepAliveNextCheckAtMs(envelope.next_check_at, nowMs, anchorMs);
|
|
130
|
+
if (nextCheckAtMs !== undefined)
|
|
131
|
+
return "next_check_at";
|
|
132
|
+
}
|
|
133
|
+
return verdict;
|
|
110
134
|
}
|
|
111
135
|
export function canResumeDoneKeepAliveLane(lane, envelope, hasQueuedWork) {
|
|
112
136
|
return doneKeepAliveVerdict(lane, envelope, hasQueuedWork) === "eligible";
|
|
@@ -105,6 +105,11 @@ export declare function buildOutpostQuestionListCommand(input?: {
|
|
|
105
105
|
}): string;
|
|
106
106
|
/** Read one question's body and owner-computed audience from its Outpost. */
|
|
107
107
|
export declare function buildOutpostQuestionShowCommand(questionId: string): string;
|
|
108
|
+
/** Cancel an owner-hosted question through the existing questions command. */
|
|
109
|
+
export declare function buildOutpostQuestionCancelCommand(input: {
|
|
110
|
+
questionId: string;
|
|
111
|
+
reason: string;
|
|
112
|
+
}): string;
|
|
108
113
|
/** Commit a queued lane move on the host that currently owns its state. */
|
|
109
114
|
export declare function buildOutpostMoveTargetCommand(input: {
|
|
110
115
|
laneId: string;
|
|
@@ -113,6 +113,18 @@ export function buildOutpostQuestionShowCommand(questionId) {
|
|
|
113
113
|
questionId,
|
|
114
114
|
]));
|
|
115
115
|
}
|
|
116
|
+
/** Cancel an owner-hosted question through the existing questions command. */
|
|
117
|
+
export function buildOutpostQuestionCancelCommand(input) {
|
|
118
|
+
return withRemoteHqDir(joinCommandParts([
|
|
119
|
+
"hq",
|
|
120
|
+
"lanes",
|
|
121
|
+
"questions",
|
|
122
|
+
"cancel",
|
|
123
|
+
input.questionId,
|
|
124
|
+
"--reason",
|
|
125
|
+
input.reason,
|
|
126
|
+
]));
|
|
127
|
+
}
|
|
116
128
|
/** Commit a queued lane move on the host that currently owns its state. */
|
|
117
129
|
export function buildOutpostMoveTargetCommand(input) {
|
|
118
130
|
return withRemoteHqDir(joinCommandParts([
|
|
@@ -24,6 +24,7 @@ export declare const RECONCILE_LOCK_TIMEOUT_MS = 5000;
|
|
|
24
24
|
/** A pickup probe that has not cleared its marker within this window is abandoned. */
|
|
25
25
|
export declare const PICKUP_VALIDATION_MAX_AGE_MS: number;
|
|
26
26
|
export declare const PICKUP_VALIDATION_TIMEOUT_REASON: "pickup_validation_timeout";
|
|
27
|
+
export declare const CLOSE_INTERRUPTED_DONE_STORY_WORKERS_FLAG = "lanes.close-interrupted-done-story-workers";
|
|
27
28
|
export declare const RECONCILE_UNIT_NAME = "hq-lanes-reconcile.service";
|
|
28
29
|
export declare const RECONCILE_TIMER_NAME = "hq-lanes-reconcile.timer";
|
|
29
30
|
export interface ReconcileSkip {
|