@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
|
@@ -75,7 +75,8 @@ export function createLaneResumeHandlers(deps) {
|
|
|
75
75
|
}
|
|
76
76
|
/**
|
|
77
77
|
* `hq lanes resume <lane-id> [--text <msg>]`: the explicit opt-in that
|
|
78
|
-
* continues a lane in `interrupted`, `done` or `blocked`,
|
|
78
|
+
* continues a lane in `interrupted`, `done` or `blocked`, an `awaiting_input`
|
|
79
|
+
* lane with no answerable pending question, or a failed lane
|
|
79
80
|
* whose only terminal reason is an invalid envelope after its repair turn,
|
|
80
81
|
* on its recorded
|
|
81
82
|
* provider session. `hq lanes message` keeps refusing `interrupted`; this
|
|
@@ -124,12 +125,21 @@ export function createLaneResumeHandlers(deps) {
|
|
|
124
125
|
throw new LanesError("worker_profile_hash_mismatch", "Profile re-pin context is only valid on an Outpost owner dispatch", { lane_id: laneId });
|
|
125
126
|
}
|
|
126
127
|
if (resolution.remoteOnly || (lane.host.kind === "outpost" && !laneIsHostedHere(ctx, lane))) {
|
|
128
|
+
const fallbackAt = lane.state === "awaiting_input" ? ctx.now().toISOString() : undefined;
|
|
127
129
|
if (!resolution.remoteOnly) {
|
|
128
130
|
await withResumeHireAdmission(ctx, lane, () => undefined);
|
|
129
131
|
}
|
|
130
132
|
await dispatchOutpostLaneCommand(ctx, lane, "resume", opts, resolution.remoteOnly
|
|
131
|
-
? {
|
|
132
|
-
|
|
133
|
+
? {
|
|
134
|
+
syncPrimary: false,
|
|
135
|
+
expectedOwnerHost: resolution.ownerHost,
|
|
136
|
+
...(fallbackAt === undefined ? {} : {
|
|
137
|
+
afterFallbackResume: () => deps.cancelFallbackQuestionsForLane(ctx, lane, "resume", fallbackAt),
|
|
138
|
+
}),
|
|
139
|
+
}
|
|
140
|
+
: fallbackAt === undefined
|
|
141
|
+
? undefined
|
|
142
|
+
: { afterFallbackResume: () => deps.cancelFallbackQuestionsForLane(ctx, lane, "resume", fallbackAt) });
|
|
133
143
|
return;
|
|
134
144
|
}
|
|
135
145
|
// Fail before provider/session work if a terminal child would consume a
|
|
@@ -171,7 +181,10 @@ export function createLaneResumeHandlers(deps) {
|
|
|
171
181
|
opts.compact === false)) {
|
|
172
182
|
throw new LanesError("worker_profile_recovery_options_invalid", "Queued worker-profile recovery accepts only --worker; rerun hq lanes resume after reconcile admits the lane", { lane_id: laneId });
|
|
173
183
|
}
|
|
174
|
-
const
|
|
184
|
+
const pendingQuestion = lane.state === "awaiting_input"
|
|
185
|
+
? await deps.hasPendingQuestionForLane(ctx, lane)
|
|
186
|
+
: false;
|
|
187
|
+
const refusal = queuedProfileRecovery || (lane.state === "awaiting_input" && !pendingQuestion)
|
|
175
188
|
? null
|
|
176
189
|
: resumeCommandRefusal(lane, { allowFailed: opts.fromFailed === true });
|
|
177
190
|
if (refusal !== null) {
|
|
@@ -444,6 +457,7 @@ export function createLaneResumeHandlers(deps) {
|
|
|
444
457
|
trigger: {
|
|
445
458
|
kind: "resume",
|
|
446
459
|
from: resumedFrom,
|
|
460
|
+
...(resumedFrom === "awaiting_input" && !pendingQuestion ? { allowAwaitingInput: true } : {}),
|
|
447
461
|
...(opts.fromFailed === true ? { fromFailed: true } : {}),
|
|
448
462
|
...(failedResumeExpectedSenior !== undefined
|
|
449
463
|
? { expectedSenior: failedResumeExpectedSenior }
|
|
@@ -460,6 +474,9 @@ export function createLaneResumeHandlers(deps) {
|
|
|
460
474
|
}, hireGrantContext);
|
|
461
475
|
}, ctx.sleep);
|
|
462
476
|
}
|
|
477
|
+
const questionCancellation = resumedFrom === "awaiting_input" && !pendingQuestion
|
|
478
|
+
? await deps.cancelFallbackQuestionsForLane(ctx, lane, "resume", at)
|
|
479
|
+
: null;
|
|
463
480
|
if (deliveryText !== undefined && messageId !== undefined) {
|
|
464
481
|
// Same audit copy a message resume leaves; nothing drains it. Archive only
|
|
465
482
|
// after a successful spawn so a concurrent resume cannot leave a phantom.
|
|
@@ -519,6 +536,10 @@ export function createLaneResumeHandlers(deps) {
|
|
|
519
536
|
story_id: next.story_id,
|
|
520
537
|
stories: laneStories(next),
|
|
521
538
|
story_warning: storyWarning(next) ?? null,
|
|
539
|
+
...(questionCancellation === null ? {} : {
|
|
540
|
+
cancelled_questions: questionCancellation.cancelled_question_ids,
|
|
541
|
+
question_cancellation_failures: questionCancellation.failures,
|
|
542
|
+
}),
|
|
522
543
|
lane: next,
|
|
523
544
|
});
|
|
524
545
|
}
|
|
@@ -137,11 +137,20 @@ export type CommandLaneResolution = {
|
|
|
137
137
|
outpostId: string;
|
|
138
138
|
ownerHost: LaneRecord["host"];
|
|
139
139
|
};
|
|
140
|
+
export interface FallbackQuestionCancellationResult {
|
|
141
|
+
cancelled_question_ids: string[];
|
|
142
|
+
failures: Array<{
|
|
143
|
+
question_id?: string;
|
|
144
|
+
error: string;
|
|
145
|
+
}>;
|
|
146
|
+
}
|
|
140
147
|
export interface OutpostLaneDispatchBehavior {
|
|
141
148
|
/** Keep remote-only lanes out of the primary lane store. */
|
|
142
149
|
syncPrimary?: boolean;
|
|
143
150
|
/** Host identity returned by the remote record lookup for a remote-only lane. */
|
|
144
151
|
expectedOwnerHost?: LaneRecord["host"];
|
|
152
|
+
/** Run after a successful remote awaiting_input fallback, before printing. */
|
|
153
|
+
afterFallbackResume?: () => Promise<FallbackQuestionCancellationResult>;
|
|
145
154
|
}
|
|
146
155
|
export interface LaneMessageHandlerDeps {
|
|
147
156
|
/** Test seam invoked after a provider worker has been started. */
|
|
@@ -177,6 +186,8 @@ export interface LaneMessageHandlerDeps {
|
|
|
177
186
|
recordResumeSetup(ctx: Ctx, lane: LaneRecord, input: RecordResumeSetupInput): LaneRecord;
|
|
178
187
|
resolveCommandLane(ctx: Ctx, laneId: string): Promise<CommandLaneResolution>;
|
|
179
188
|
resolveActiveQuestionCompany(ctx: Ctx, message?: string): Promise<LaneCompanyRef>;
|
|
189
|
+
hasPendingQuestionForLane(ctx: Ctx, lane: LaneRecord): Promise<boolean>;
|
|
190
|
+
cancelFallbackQuestionsForLane(ctx: Ctx, lane: LaneRecord, trigger: "message" | "resume", at: string): Promise<FallbackQuestionCancellationResult>;
|
|
180
191
|
resolveResumeProfile(ctx: Ctx, lane: LaneRecord, workerId: string): ResolvedResumeProfile;
|
|
181
192
|
selectAutomaticClaudeResumeAccount(ctx: Ctx, lane: LaneRecord, workerProfile?: ResolvedResumeProfile, options?: {
|
|
182
193
|
repairUnavailableAccount?: boolean;
|
|
@@ -188,7 +199,7 @@ export interface LaneMessageHandlerDeps {
|
|
|
188
199
|
validateResumeProfileModel(ctx: Ctx, lane: LaneRecord, profile: WorkerProfile): void;
|
|
189
200
|
withResumeHireAdmission<T>(ctx: Ctx, lane: LaneRecord, fn: () => T, grantContext?: OutpostHireGrantContext): Promise<T>;
|
|
190
201
|
}
|
|
191
|
-
export interface LaneResumeHandlerDeps extends Pick<LaneMessageHandlerDeps, "onLaneWorkerStarted" | "archiveResumeMessage" | "assertLaneRunsHereForAdmission" | "assertLaneRunsHereForOperatorMutation" | "assessLaneAdmission" | "casUpdateLane" | "laneIsHostedHere" | "laneWithRecoveredProviderSession" | "mintMessageId" | "prepareAccountMove" | "persistClaudeAccountSelection" | "printJson" | "recordResumeSetup" | "resolveCommandLane" | "resolveResumeProfile" | "selectAutomaticClaudeResumeAccount" | "validateResumeProfileModel" | "withResumeHireAdmission"> {
|
|
202
|
+
export interface LaneResumeHandlerDeps extends Pick<LaneMessageHandlerDeps, "onLaneWorkerStarted" | "archiveResumeMessage" | "assertLaneRunsHereForAdmission" | "assertLaneRunsHereForOperatorMutation" | "assessLaneAdmission" | "casUpdateLane" | "laneIsHostedHere" | "laneWithRecoveredProviderSession" | "mintMessageId" | "prepareAccountMove" | "persistClaudeAccountSelection" | "printJson" | "recordResumeSetup" | "resolveCommandLane" | "hasPendingQuestionForLane" | "cancelFallbackQuestionsForLane" | "resolveResumeProfile" | "selectAutomaticClaudeResumeAccount" | "validateResumeProfileModel" | "withResumeHireAdmission"> {
|
|
192
203
|
dispatchOutpostLaneCommand(ctx: Ctx, lane: LaneRecord, action: "resume", options: LaneResumeOptions, behavior?: OutpostLaneDispatchBehavior): Promise<void>;
|
|
193
204
|
authorizeFailedResume(ctx: Ctx, lane: LaneRecord, encodedContext?: string): string;
|
|
194
205
|
authorizeResumeProfileRepin(ctx: Ctx, lane: LaneRecord, profile: ResolvedResumeProfile | undefined, encodedContext: string | undefined): ResumeProfileRepinAuthorization | undefined;
|
|
@@ -382,6 +393,7 @@ export interface LaneQuestionsHandlerDeps {
|
|
|
382
393
|
assertQuestionAnswerAuthorized(ctx: Ctx, lane: LaneRecord): void;
|
|
383
394
|
assertQuestionCompanyMatchesLane(question: QuestionRecord, lane: Pick<LaneRecord, "lane_id" | "company" | "mesh_company" | "project_id">, code: string, hqRoot?: string): void;
|
|
384
395
|
casUpdateQuestion(hqRoot: string, questionId: string, updater: (current: QuestionRecord) => QuestionRecord): QuestionRecord;
|
|
396
|
+
casUpdateLane(hqRoot: string, laneId: string, updater: (current: LaneRecord) => LaneRecord): LaneRecord;
|
|
385
397
|
effectiveLaneQuestionCompany(lane: Pick<LaneRecord, "company" | "mesh_company" | "project_id" | "lane_id">, hqRoot?: string): LaneCompanyRef | null;
|
|
386
398
|
emitLaneMeshEvent(ctx: Ctx, company: LaneCompanyRef | null | undefined, threadId: string | undefined, event: LaneMeshEvent, payload: Record<string, unknown>): Promise<void>;
|
|
387
399
|
groupQuestionLanesByCompany(hqRoot: string, lanes: LaneRecord[], fallbackCompany?: LaneCompanyRef): QuestionCompanyGroup[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { accountStatus, coveredLaneIds, loadLanesConfig, MAX_WATCHED_LANES, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, monitorReminderText, runLaneWatch, } from "../lib/lanes/index.js";
|
|
2
|
-
import { LEAD_LANE_KEEP_ALIVE_FLAG } from "../lib/lanes/keep-alive.js";
|
|
2
|
+
import { LEAD_LANE_KEEP_ALIVE_FLAG, LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_FLAG } from "../lib/lanes/keep-alive.js";
|
|
3
3
|
import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
|
|
4
4
|
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
5
5
|
import { isLanesError, LanesError } from "../lib/lanes/errors.js";
|
|
@@ -50,6 +50,10 @@ export function createLaneWatchHandlers(deps) {
|
|
|
50
50
|
company: resolveLaneCompany(hqRoot, lane).company ?? undefined,
|
|
51
51
|
env: ctx.env,
|
|
52
52
|
}),
|
|
53
|
+
keepAliveNextCheckAtEnabled: (lane) => readLanesRegistryFlag(LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_FLAG, {
|
|
54
|
+
company: resolveLaneCompany(hqRoot, lane).company ?? undefined,
|
|
55
|
+
env: ctx.env,
|
|
56
|
+
}),
|
|
53
57
|
readAccountStatuses: async (lanes) => {
|
|
54
58
|
if (lanes.length === 0)
|
|
55
59
|
return [];
|
package/dist/commands/lanes.js
CHANGED
|
@@ -55,11 +55,11 @@ import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, lan
|
|
|
55
55
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
56
56
|
import { listLaneReviews } from "../lib/lanes/reviews.js";
|
|
57
57
|
import { effectiveAccountHome } from "../lib/lanes/account-home.js";
|
|
58
|
-
import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readAllLanes, readLane, sanitizeHostComponent, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, readWorkerRunResult, resumeCommandRefusal, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
|
|
58
|
+
import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readAllLanes, readLane, sanitizeHostComponent, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, DEAD_HOST_MS, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, readWorkerRunResult, resumeCommandRefusal, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
|
|
59
59
|
import { laneStories, storyWarning, } from "../lib/lanes/stories.js";
|
|
60
60
|
import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
|
|
61
61
|
import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
|
|
62
|
-
import { doneKeepAliveVerdict, hasPendingKeepAliveWork, keepAliveExclusion, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
|
|
62
|
+
import { doneKeepAliveVerdict, hasPendingKeepAliveWork, keepAliveNextCheckAtMs, keepAliveExclusion, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_FLAG, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
|
|
63
63
|
import { questionBodyAbsPath, resolveQuestionCompanyWithFallback, resolveProjectQuestionCompany, } from "../lib/lanes/questions.js";
|
|
64
64
|
import { assertQuestionBodySecretSafe } from "../lib/lanes/questions.js";
|
|
65
65
|
import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
@@ -1260,6 +1260,8 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds, opt
|
|
|
1260
1260
|
unverifiedRemote: new Map(),
|
|
1261
1261
|
missingOnOwner: new Map(),
|
|
1262
1262
|
};
|
|
1263
|
+
const now = ctx.now();
|
|
1264
|
+
const nowMs = now.getTime();
|
|
1263
1265
|
const host = reconcileHostFromCtx(ctx);
|
|
1264
1266
|
if (ctx.env.HQ_LANES_REMOTE_READ === "1" || parentLaneIds?.size === 0)
|
|
1265
1267
|
return result;
|
|
@@ -1282,6 +1284,9 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds, opt
|
|
|
1282
1284
|
((parentLaneIds === undefined || (lane.senior.kind === "lane" &&
|
|
1283
1285
|
lane.senior.id !== undefined &&
|
|
1284
1286
|
parentLaneIds.has(lane.senior.id))) || options.laneIds?.has(lane.lane_id) === true) &&
|
|
1287
|
+
!(isTerminalLaneState(lane.state) &&
|
|
1288
|
+
lane.terminal_reason === DISPATCH_TERMINAL_REASON &&
|
|
1289
|
+
lane.last_host_report_at === undefined) &&
|
|
1285
1290
|
(options.includeTerminalMirrors === true || !isTerminalLaneState(lane.state)))
|
|
1286
1291
|
.sort((a, b) => {
|
|
1287
1292
|
const terminalOrder = Number(isTerminalLaneState(a.state)) - Number(isTerminalLaneState(b.state));
|
|
@@ -1389,11 +1394,20 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds, opt
|
|
|
1389
1394
|
result.updated.set(mirror.lane_id, before);
|
|
1390
1395
|
continue;
|
|
1391
1396
|
}
|
|
1397
|
+
const ownerReportAtMs = Date.parse(owner.last_host_report_at ?? "");
|
|
1398
|
+
const ownerReportIsFresh = owner.state === "running" &&
|
|
1399
|
+
Number.isFinite(ownerReportAtMs) &&
|
|
1400
|
+
ownerReportAtMs <= nowMs + 60_000 &&
|
|
1401
|
+
nowMs - ownerReportAtMs < DEAD_HOST_MS;
|
|
1402
|
+
const mirroredReportAt = ownerReportIsFresh
|
|
1403
|
+
? new Date(Math.min(ownerReportAtMs, nowMs)).toISOString()
|
|
1404
|
+
: undefined;
|
|
1392
1405
|
const needsUpdate = before.state !== owner.state ||
|
|
1393
1406
|
before.queued_reason !== (owner.state === "queued" ? owner.queued_reason : undefined) ||
|
|
1394
1407
|
JSON.stringify(before.queued_details) !== JSON.stringify(owner.state === "queued" ? owner.queued_details : undefined) ||
|
|
1395
1408
|
before.ended_at !== endedAt ||
|
|
1396
1409
|
before.envelope_summary !== envelopeSummary ||
|
|
1410
|
+
(mirroredReportAt !== undefined && before.last_host_report_at !== mirroredReportAt) ||
|
|
1397
1411
|
(isTerminalLaneState(owner.state) && (JSON.stringify(before.last_envelope) !== JSON.stringify(owner.last_envelope) ||
|
|
1398
1412
|
before.story_complete !== owner.story_complete ||
|
|
1399
1413
|
before.story_complete_reason !== owner.story_complete_reason ||
|
|
@@ -1420,6 +1434,8 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds, opt
|
|
|
1420
1434
|
queued_details: owner.state === "queued" ? owner.queued_details : undefined,
|
|
1421
1435
|
ended_at: endedAt,
|
|
1422
1436
|
envelope_summary: envelopeSummary,
|
|
1437
|
+
// Preserve a fresh owner report; clamp small clock skew to the primary's time.
|
|
1438
|
+
...(mirroredReportAt !== undefined ? { last_host_report_at: mirroredReportAt } : {}),
|
|
1423
1439
|
...(isTerminalLaneState(owner.state) ? {
|
|
1424
1440
|
last_envelope: owner.last_envelope,
|
|
1425
1441
|
story_complete: owner.story_complete,
|
|
@@ -4261,10 +4277,29 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
|
|
|
4261
4277
|
remote.delivery_status === "queued_on_target_account" ||
|
|
4262
4278
|
remote.delivery_status === "queued_on_admission")) {
|
|
4263
4279
|
const ownerLane = dispatchedOwnerLane(ctx, lane.lane_id, outpostId, remote.lane, behavior);
|
|
4280
|
+
const questionCancellation = lane.state === "awaiting_input" && behavior.afterFallbackResume
|
|
4281
|
+
? await behavior.afterFallbackResume()
|
|
4282
|
+
: undefined;
|
|
4283
|
+
const ownerCancelledQuestions = Array.isArray(remote.cancelled_questions)
|
|
4284
|
+
? remote.cancelled_questions.filter((id) => typeof id === "string")
|
|
4285
|
+
: [];
|
|
4286
|
+
const ownerCancellationFailures = Array.isArray(remote.question_cancellation_failures)
|
|
4287
|
+
? remote.question_cancellation_failures
|
|
4288
|
+
: [];
|
|
4264
4289
|
printJson({
|
|
4265
4290
|
...remote,
|
|
4266
4291
|
lane_id: ownerLane.lane_id,
|
|
4267
4292
|
lane: ownerLane,
|
|
4293
|
+
...(questionCancellation === undefined ? {} : {
|
|
4294
|
+
cancelled_questions: [...new Set([
|
|
4295
|
+
...ownerCancelledQuestions,
|
|
4296
|
+
...questionCancellation.cancelled_question_ids,
|
|
4297
|
+
])],
|
|
4298
|
+
question_cancellation_failures: [
|
|
4299
|
+
...ownerCancellationFailures,
|
|
4300
|
+
...questionCancellation.failures,
|
|
4301
|
+
],
|
|
4302
|
+
}),
|
|
4268
4303
|
outpost_id: outpostId,
|
|
4269
4304
|
outpost_command_id: dispatched.commandId,
|
|
4270
4305
|
outpost_transport: "outposts_exec",
|
|
@@ -4281,10 +4316,29 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
|
|
|
4281
4316
|
descendants.push(syncLaneRecordFromOwner(ctx.hqRoot(), descendantId, outpostId, ownerDescendant, undefined, { allowNonTerminal: true }));
|
|
4282
4317
|
}
|
|
4283
4318
|
}
|
|
4319
|
+
const questionCancellation = action === "resume" && lane.state === "awaiting_input" && behavior.afterFallbackResume
|
|
4320
|
+
? await behavior.afterFallbackResume()
|
|
4321
|
+
: undefined;
|
|
4322
|
+
const ownerCancelledQuestions = Array.isArray(remote.cancelled_questions)
|
|
4323
|
+
? remote.cancelled_questions.filter((id) => typeof id === "string")
|
|
4324
|
+
: [];
|
|
4325
|
+
const ownerCancellationFailures = Array.isArray(remote.question_cancellation_failures)
|
|
4326
|
+
? remote.question_cancellation_failures
|
|
4327
|
+
: [];
|
|
4284
4328
|
printJson({
|
|
4285
4329
|
...remote,
|
|
4286
4330
|
lane_id: ownerLane.lane_id,
|
|
4287
4331
|
lane: ownerLane,
|
|
4332
|
+
...(questionCancellation === undefined ? {} : {
|
|
4333
|
+
cancelled_questions: [...new Set([
|
|
4334
|
+
...ownerCancelledQuestions,
|
|
4335
|
+
...questionCancellation.cancelled_question_ids,
|
|
4336
|
+
])],
|
|
4337
|
+
question_cancellation_failures: [
|
|
4338
|
+
...ownerCancellationFailures,
|
|
4339
|
+
...questionCancellation.failures,
|
|
4340
|
+
],
|
|
4341
|
+
}),
|
|
4288
4342
|
...(action === "reparent"
|
|
4289
4343
|
? { changed_descendant_ids: changedDescendantIds, descendants }
|
|
4290
4344
|
: {}),
|
|
@@ -5009,7 +5063,7 @@ async function refreshOutpostLaneCopiesForShow(ctx, lane, localLanes) {
|
|
|
5009
5063
|
const refreshedLane = refreshed[0] ?? null;
|
|
5010
5064
|
return refreshedLane ?? lane;
|
|
5011
5065
|
}
|
|
5012
|
-
async function runShow(ctx, laneId) {
|
|
5066
|
+
async function runShow(ctx, laneId, hasPendingQuestionForLane) {
|
|
5013
5067
|
const hqRoot = ctx.hqRoot();
|
|
5014
5068
|
const initialLane = readLane(hqRoot, laneId);
|
|
5015
5069
|
const initialLanes = loadAllLanes(hqRoot);
|
|
@@ -5023,10 +5077,24 @@ async function runShow(ctx, laneId) {
|
|
|
5023
5077
|
? { ...storedLane, state: "remote", remote_state: storedLane.state, queued_reason: undefined, queued_details: undefined }
|
|
5024
5078
|
: storedLane;
|
|
5025
5079
|
const askDeliveryFailed = readJsonFile(askDeliveryFailedPath(hqRoot, laneId));
|
|
5080
|
+
const delivery = storedLane.state === "awaiting_input" &&
|
|
5081
|
+
await hasPendingQuestionForLane(ctx, storedLane)
|
|
5082
|
+
? "questions"
|
|
5083
|
+
: laneDeliveryMechanism(storedLane);
|
|
5026
5084
|
const keepAliveState = resolveLaneKeepAlive(hqRoot, storedLane);
|
|
5027
5085
|
const keepAliveCompany = resolveLaneCompany(hqRoot, storedLane);
|
|
5086
|
+
const now = ctx.now().getTime();
|
|
5087
|
+
const nextCheckAtEnabled = storedLane.state === "done" && storedLane.keep_alive === true
|
|
5088
|
+
? await readLanesRegistryFlag(LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_FLAG, {
|
|
5089
|
+
company: keepAliveCompany.company ?? undefined,
|
|
5090
|
+
env: ctx.env,
|
|
5091
|
+
})
|
|
5092
|
+
: false;
|
|
5093
|
+
const hasPendingWork = storedLane.keep_alive_standing === true || nextCheckAtEnabled
|
|
5094
|
+
? hasPendingKeepAliveWork(hqRoot, storedLane)
|
|
5095
|
+
: false;
|
|
5028
5096
|
const doneStateVerdict = storedLane.state === "done" && storedLane.keep_alive === true
|
|
5029
|
-
? doneKeepAliveVerdict(storedLane, { decision: storedLane.last_envelope?.decision, story_complete: storedLane.story_complete }, storedLane.keep_alive_standing === true &&
|
|
5097
|
+
? doneKeepAliveVerdict(storedLane, { decision: storedLane.last_envelope?.decision, story_complete: storedLane.story_complete, next_check_at: storedLane.last_envelope?.next_check_at }, storedLane.keep_alive_standing === true && hasPendingWork, { nextCheckAtEnabled, hasPendingWork, nowMs: now })
|
|
5030
5098
|
: null;
|
|
5031
5099
|
let managerStatusTarget;
|
|
5032
5100
|
let managerStatusGateError;
|
|
@@ -5048,7 +5116,15 @@ async function runShow(ctx, laneId) {
|
|
|
5048
5116
|
now: ctx.now().getTime(),
|
|
5049
5117
|
});
|
|
5050
5118
|
const laneIsSenior = allLanes.some((item) => item.senior.kind === "lane" && item.senior.id === laneId);
|
|
5051
|
-
const
|
|
5119
|
+
const requestedNextCheckMs = nextCheckAtEnabled && !hasPendingWork
|
|
5120
|
+
? keepAliveNextCheckAtMs(storedLane.last_envelope?.next_check_at, now, Date.parse(storedLane.last_envelope?.at ?? storedLane.updated_at))
|
|
5121
|
+
: undefined;
|
|
5122
|
+
const storedNextEligibleMs = Date.parse(storedLane.keep_alive_next_eligible_at ?? "");
|
|
5123
|
+
const nextEligibleAt = Number.isFinite(storedNextEligibleMs) && requestedNextCheckMs !== undefined
|
|
5124
|
+
? new Date(Math.max(storedNextEligibleMs, requestedNextCheckMs)).toISOString()
|
|
5125
|
+
: requestedNextCheckMs !== undefined
|
|
5126
|
+
? new Date(requestedNextCheckMs).toISOString()
|
|
5127
|
+
: storedLane.keep_alive_next_eligible_at ?? null;
|
|
5052
5128
|
const reviewListing = laneIsSenior
|
|
5053
5129
|
? listLaneReviews(hqRoot, laneId)
|
|
5054
5130
|
: { reviews: [], skipped: 0 };
|
|
@@ -5075,7 +5151,7 @@ async function runShow(ctx, laneId) {
|
|
|
5075
5151
|
stalled_at: lane.stalled_at ?? null,
|
|
5076
5152
|
stalled_reason: lane.stalled_reason ?? null,
|
|
5077
5153
|
ask_delivery_failed: askDeliveryFailed,
|
|
5078
|
-
delivery
|
|
5154
|
+
delivery,
|
|
5079
5155
|
provider_session_id: lane.provider_session_id ?? null,
|
|
5080
5156
|
provider_session_id_absent: lane.provider_session_id_absent ?? null,
|
|
5081
5157
|
envelope_repair_mode: lane.envelope_repair?.mode ?? null,
|
|
@@ -5108,7 +5184,7 @@ async function runShow(ctx, laneId) {
|
|
|
5108
5184
|
consecutive_auto_resumes: lane.keep_alive_resume_count ?? 0,
|
|
5109
5185
|
last_auto_resume_at: lane.keep_alive_last_resume_at ?? null,
|
|
5110
5186
|
keep_alive_history: lane.keep_alive_history ?? [],
|
|
5111
|
-
next_eligible_at:
|
|
5187
|
+
next_eligible_at: nextEligibleAt,
|
|
5112
5188
|
exhausted_at: lane.keep_alive_exhausted_at ?? null,
|
|
5113
5189
|
},
|
|
5114
5190
|
manager_status_gate: managerStatusGateError !== undefined
|
|
@@ -5134,6 +5210,7 @@ async function runShow(ctx, laneId) {
|
|
|
5134
5210
|
escalated: blockerFreshness.stale.length > 0 &&
|
|
5135
5211
|
storedLane.blocker_freshness_state?.escalated_fingerprint ===
|
|
5136
5212
|
blockerFreshnessFingerprint(blockerFreshness.stale),
|
|
5213
|
+
undeliverable_reason: storedLane.blocker_freshness_state?.undeliverable_reason ?? null,
|
|
5137
5214
|
}
|
|
5138
5215
|
: null,
|
|
5139
5216
|
kr_freshness: krFreshness,
|
|
@@ -7807,6 +7884,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
7807
7884
|
assertQuestionAnswerAuthorized,
|
|
7808
7885
|
assertQuestionCompanyMatchesLane,
|
|
7809
7886
|
casUpdateQuestion,
|
|
7887
|
+
casUpdateLane,
|
|
7810
7888
|
effectiveLaneQuestionCompany,
|
|
7811
7889
|
emitLaneMeshEvent,
|
|
7812
7890
|
groupQuestionLanesByCompany,
|
|
@@ -7857,6 +7935,8 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
7857
7935
|
recordResumeSetup,
|
|
7858
7936
|
resolveCommandLane: (commandCtx, laneId) => resolveCommandLaneTarget(commandCtx, laneId),
|
|
7859
7937
|
resolveActiveQuestionCompany,
|
|
7938
|
+
hasPendingQuestionForLane: questionHandlers.hasPendingQuestionForLane,
|
|
7939
|
+
cancelFallbackQuestionsForLane: questionHandlers.cancelFallbackQuestionsForLane,
|
|
7860
7940
|
resolveResumeProfile,
|
|
7861
7941
|
selectAutomaticClaudeResumeAccount,
|
|
7862
7942
|
validateResumeProfileModel,
|
|
@@ -7882,6 +7962,8 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
7882
7962
|
printJson,
|
|
7883
7963
|
recordResumeSetup,
|
|
7884
7964
|
resolveCommandLane: (commandCtx, laneId) => resolveCommandLaneTarget(commandCtx, laneId),
|
|
7965
|
+
hasPendingQuestionForLane: questionHandlers.hasPendingQuestionForLane,
|
|
7966
|
+
cancelFallbackQuestionsForLane: questionHandlers.cancelFallbackQuestionsForLane,
|
|
7885
7967
|
resolveResumeProfile,
|
|
7886
7968
|
selectAutomaticClaudeResumeAccount,
|
|
7887
7969
|
validateResumeProfileModel,
|
|
@@ -8153,7 +8235,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8153
8235
|
.description("Show one lane")
|
|
8154
8236
|
.argument("<lane-id>", "Lane id")
|
|
8155
8237
|
.action(wrap(async (laneId) => {
|
|
8156
|
-
await runShow(ctx, laneId);
|
|
8238
|
+
await runShow(ctx, laneId, questionHandlers.hasPendingQuestionForLane);
|
|
8157
8239
|
}));
|
|
8158
8240
|
const stories = lanes
|
|
8159
8241
|
.command("story")
|
|
@@ -8290,7 +8372,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8290
8372
|
}));
|
|
8291
8373
|
lanes
|
|
8292
8374
|
.command("resume")
|
|
8293
|
-
.description("Resume an interrupted, done or blocked lane
|
|
8375
|
+
.description("Resume an interrupted, done or blocked lane, or an awaiting_input lane with no answerable question; recover failed lanes with the senior-only --from-failed option")
|
|
8294
8376
|
.argument("<lane-id>", "Lane id")
|
|
8295
8377
|
.option("--text <msg>", "Operator message for the resumed turn")
|
|
8296
8378
|
.option("--from-failed", "Explicitly resume a failed lane as its senior")
|
package/dist/commands/monitor.js
CHANGED
|
@@ -278,6 +278,7 @@ async function startMonitor(options, argv = []) {
|
|
|
278
278
|
cwd: meta.cwd,
|
|
279
279
|
env: { ...env, HQ_ROOT: hqRoot },
|
|
280
280
|
detached: true,
|
|
281
|
+
windowsHide: true,
|
|
281
282
|
stdio: "ignore",
|
|
282
283
|
});
|
|
283
284
|
child.once("error", (err) => {
|
|
@@ -347,7 +348,21 @@ async function showMonitor(id, json) {
|
|
|
347
348
|
const state = stateForDisplay(hqRoot, id, readJson(statePath(hqRoot, id)));
|
|
348
349
|
if (!meta || !state)
|
|
349
350
|
throw new LanesError("monitor_not_found", `Monitor ${id} was not found.`, { monitor_id: id });
|
|
350
|
-
const events =
|
|
351
|
+
const events = [];
|
|
352
|
+
for (const line of fs.readFileSync(path.join(dir, "events.jsonl"), "utf8").split("\n")) {
|
|
353
|
+
if (!line.trim())
|
|
354
|
+
continue;
|
|
355
|
+
let parsed;
|
|
356
|
+
try {
|
|
357
|
+
parsed = JSON.parse(line);
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
363
|
+
continue;
|
|
364
|
+
events.push(parsed);
|
|
365
|
+
}
|
|
351
366
|
if (json)
|
|
352
367
|
printJson({ meta, state, events });
|
|
353
368
|
else
|
|
@@ -712,7 +727,7 @@ export async function runSupervisor(id, deps = {}) {
|
|
|
712
727
|
const args = meta.command !== null ? childArgs : childArgs.slice(1);
|
|
713
728
|
if (!executable)
|
|
714
729
|
throw new LanesError("monitor_command_required", "No command was recorded for this monitor.");
|
|
715
|
-
commandChild = (deps.spawn ?? spawn)(executable, args, { cwd: meta.cwd, env: process.env, detached: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
730
|
+
commandChild = (deps.spawn ?? spawn)(executable, args, { cwd: meta.cwd, env: process.env, detached: true, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
716
731
|
if (commandChild.pid) {
|
|
717
732
|
state = { ...state, child_pid: commandChild.pid, child_starttime: readProcStarttime(commandChild.pid) };
|
|
718
733
|
writeJsonAtomic(statePath(hqRoot, id), state);
|
package/dist/commands/report.js
CHANGED
|
@@ -6,6 +6,7 @@ import * as path from "node:path";
|
|
|
6
6
|
import { ensureCognitoToken, } from "../utils/cognito-session.js";
|
|
7
7
|
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
8
8
|
import { DEPLOY_API_URL, deployApiRequest, deployHeaders } from "../lib/deploy-api.js";
|
|
9
|
+
import { listDeployApps } from "../lib/deploy-apps.js";
|
|
9
10
|
import { listReportTemplates } from "../lib/report-templates.js";
|
|
10
11
|
import { adoptReport, buildReportTarball, conventionReportDir, deployOrgContext, ReportPublishError, plainFailureReason, publishReport, } from "../lib/report-publish.js";
|
|
11
12
|
const REPORT_CHANNEL_STATE_FILE = "report-channels.json";
|
|
@@ -259,8 +260,10 @@ async function orgFor(token, report) {
|
|
|
259
260
|
return deployOrgContext(report, slug);
|
|
260
261
|
}
|
|
261
262
|
export async function findOrCreateApp(token, slug, org) {
|
|
262
|
-
const
|
|
263
|
-
|
|
263
|
+
const apps = await listDeployApps((cursor) => {
|
|
264
|
+
const route = cursor ? `/api/apps?cursor=${encodeURIComponent(cursor)}` : "/api/apps";
|
|
265
|
+
return deployJson(token, route, "GET", undefined, org.headers);
|
|
266
|
+
});
|
|
264
267
|
const mine = (name) => apps.find((app) => !!app && typeof app === "object" && (app.subdomain === name || app.name === name));
|
|
265
268
|
let app;
|
|
266
269
|
for (const name of candidateSlugs(slug)) {
|
|
@@ -45,5 +45,9 @@ export declare function shouldAttemptSelfUpdate(opts: {
|
|
|
45
45
|
check?: boolean;
|
|
46
46
|
selfUpdate?: boolean;
|
|
47
47
|
}): boolean;
|
|
48
|
+
/** Latest release tag for a repo, e.g. `v12.3.0`. Throws on failure. */
|
|
49
|
+
export declare function getLatestReleaseTag(repo: string, token?: string): Promise<string>;
|
|
50
|
+
/** Resolve a tag to its commit SHA, dereferencing annotated tags. */
|
|
51
|
+
export declare function resolveTagSha(repo: string, tag: string, token?: string): Promise<string>;
|
|
48
52
|
export declare function registerRescueCommand(program: Command): void;
|
|
49
53
|
//# sourceMappingURL=rescue.d.ts.map
|
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,
|
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. */
|
|
@@ -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
|
|
@@ -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
|