@indigoai-us/hq-cli 5.345.55 → 5.345.57
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 +18 -0
- package/assets/scaffold/core/scripts/derive-trigger-facts.sh +10 -1
- package/assets/scaffold/core/scripts/jobs-validate.sh +2 -1
- package/assets/scaffold/core/scripts/session-title.sh +18 -15
- package/dist/command-catalog.generated.d.ts +124 -0
- package/dist/command-catalog.generated.js +155 -0
- package/dist/commands/core-delegate-w1b.js +18 -5
- package/dist/commands/core-derive-trigger-facts-command.js +14 -0
- package/dist/commands/core-derive-trigger-facts-native.d.ts +1 -0
- package/dist/commands/core-derive-trigger-facts-native.js +20 -1
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +15 -2
- package/dist/commands/lanes-checks.d.ts +3 -0
- package/dist/commands/lanes-checks.js +34 -0
- package/dist/commands/lanes-loop.d.ts +12 -0
- package/dist/commands/lanes-loop.js +214 -0
- package/dist/commands/lanes-questions.js +37 -11
- package/dist/commands/lanes-resume.js +19 -1
- package/dist/commands/lanes-session-start.d.ts +2 -7
- package/dist/commands/lanes-session-start.js +9 -29
- package/dist/commands/lanes-shared.d.ts +14 -2
- package/dist/commands/lanes.d.ts +3 -1
- package/dist/commands/lanes.js +376 -35
- package/dist/commands/members.d.ts +4 -0
- package/dist/commands/members.js +23 -2
- package/dist/lanes-drain-hook.js +6 -2
- package/dist/lib/doctor/checks/sync-health.d.ts +2 -0
- package/dist/lib/doctor/checks/sync-health.js +47 -23
- package/dist/lib/lanes/delivery.d.ts +3 -0
- package/dist/lib/lanes/delivery.js +5 -0
- package/dist/lib/lanes/dropbox.d.ts +7 -2
- package/dist/lib/lanes/dropbox.js +157 -10
- package/dist/lib/lanes/flag-keys.d.ts +12 -0
- package/dist/lib/lanes/flag-keys.js +12 -0
- package/dist/lib/lanes/hooks-install.d.ts +14 -11
- package/dist/lib/lanes/hooks-install.js +70 -50
- package/dist/lib/lanes/loop.d.ts +158 -0
- package/dist/lib/lanes/loop.js +743 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +9 -1
- package/dist/lib/lanes/outpost-dispatch.js +21 -7
- package/dist/lib/lanes/reaper.d.ts +58 -0
- package/dist/lib/lanes/reaper.js +403 -0
- package/dist/lib/lanes/reconcile.d.ts +19 -0
- package/dist/lib/lanes/reconcile.js +173 -6
- package/dist/lib/lanes/spawn.d.ts +4 -0
- package/dist/lib/lanes/spawn.js +41 -9
- package/dist/lib/lanes/status-fields.d.ts +12 -0
- package/dist/lib/lanes/status-fields.js +189 -0
- package/dist/lib/lanes/store.js +49 -2
- package/dist/lib/lanes/types.d.ts +18 -0
- package/dist/lib/lanes/types.js +3 -3
- package/dist/lib/lanes/watcher.js +3 -0
- package/dist/lib/lanes/worker-continuity.d.ts +18 -0
- package/dist/lib/lanes/worker-continuity.js +81 -0
- package/dist/lib/lanes/worker-process.d.ts +4 -0
- package/dist/lib/lanes/worker-process.js +77 -0
- package/dist/main.js +9 -3
- package/dist/utils/cli-telemetry.d.ts +4 -1
- package/dist/utils/cli-telemetry.js +8 -0
- package/dist/utils/jq-windows-compat.d.ts +13 -0
- package/dist/utils/jq-windows-compat.js +48 -0
- package/dist/utils/run-bundled-script.d.ts +3 -0
- package/dist/utils/run-bundled-script.js +9 -80
- package/dist/utils/windows-bash.d.ts +5 -0
- package/dist/utils/windows-bash.js +75 -0
- package/package.json +1 -1
package/dist/commands/lanes.js
CHANGED
|
@@ -38,12 +38,18 @@ import { laneDepthCap } from "../lib/lanes/depth-cap.js";
|
|
|
38
38
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
39
39
|
import { runResidentCommandWithPackageLease } from "../utils/resident-package-preload.js";
|
|
40
40
|
import { readLanesFlagLocalOverride, readLanesRegistryFlag, readLanesRegistryFlagFailOff, readLanesRegistryFlagFailOpen } from "../lib/lanes/registry-flag.js";
|
|
41
|
+
import { readLaneStatusRowFields } from "../lib/lanes/status-fields.js";
|
|
41
42
|
import { appendProjectFocusOverride, readProjectStoryFocusData, removeProjectFocusOverride } from "../lib/lanes/story-ledger.js";
|
|
42
43
|
import { appendLaneFocusOverride, assertFocusGuardAllows, makeFocusOverride } from "../lib/lanes/focus-guard.js";
|
|
43
|
-
import { LANE_FOCUS_GUARD_FLAG, LANE_FOCUS_DASHBOARD_FLAG, LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG, LANE_REUSE_REFRESHES_PROFILE_FLAG, } from "../lib/lanes/flag-keys.js";
|
|
44
|
+
import { LANE_FOCUS_GUARD_FLAG, LANE_FOCUS_DASHBOARD_FLAG, LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG, LANE_STATUS_ROW_FIELDS_FLAG, LANE_REAP_STOP_FLAG, LANE_REUSE_REFRESHES_PROFILE_FLAG, LANE_WORKER_CONTINUITY_FLAG, LANE_CONDUCT_LOOP_FLAG, } from "../lib/lanes/flag-keys.js";
|
|
45
|
+
import { findWorkerContinuityLane, sameLaneHost, workerContinuityPrompt } from "../lib/lanes/worker-continuity.js";
|
|
46
|
+
import { enqueueLoopEnvelope, interruptLoopQueue, loopControllerIsAlive, loopStatus, validateLoopEnvelope } from "../lib/lanes/loop.js";
|
|
47
|
+
import { runLaneLoopController, startLaneLoopController } from "./lanes-loop.js";
|
|
44
48
|
import { configuredCodexRolloutHomes, retainCodexRollouts } from "../lib/lanes/disk-guard.js";
|
|
45
49
|
import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
|
|
46
50
|
import { runLeadAutopilotPass } from "../lib/lanes/autopilot.js";
|
|
51
|
+
import { appendDropboxMessageOnce, appendDropboxMessageOnceAtPath } from "../lib/lanes/dropbox.js";
|
|
52
|
+
import { monitorSessionDir } from "../lib/monitor.js";
|
|
47
53
|
import { syncOutpostLeadRegistries } from "../lib/lanes/autopilot-registry.js";
|
|
48
54
|
import { OutpostSnapshotInvocationMemo } from "../lib/lanes/outpost-snapshot-memo.js";
|
|
49
55
|
import { createOutpostHireGrantContext, parseOutpostHireGrantContext, selectOutpostHireGrantHierarchy, sameHireGrantSenior, } from "../lib/lanes/hire-grant-dispatch.js";
|
|
@@ -462,6 +468,7 @@ function resolveDeps(deps = {}) {
|
|
|
462
468
|
focusGuardEnabled: deps.focusGuardEnabled,
|
|
463
469
|
isSsmOwnerDispatch: deps.isSsmOwnerDispatch ?? (() => isSsmRunCommandProcess()),
|
|
464
470
|
withGrantHireLocks: deps.withGrantHireLocks ?? withLaneHireLocks,
|
|
471
|
+
withAdmissionLock: deps.withAdmissionLock ?? withLaneAdmissionLock,
|
|
465
472
|
mergeReviewEnabled: deps.mergeReviewEnabled,
|
|
466
473
|
mergeHoldEnabled: deps.mergeHoldEnabled,
|
|
467
474
|
};
|
|
@@ -3323,6 +3330,16 @@ async function runCreate(ctx, opts) {
|
|
|
3323
3330
|
}
|
|
3324
3331
|
meshCompany = resolvedMeshCompany;
|
|
3325
3332
|
}
|
|
3333
|
+
if (opts.loop === true && !(await readLanesRegistryFlagFailOpen(LANE_CONDUCT_LOOP_FLAG, {
|
|
3334
|
+
company: company ?? meshCompany,
|
|
3335
|
+
defaultValue: true,
|
|
3336
|
+
env: ctx.env,
|
|
3337
|
+
}))) {
|
|
3338
|
+
throw new LanesError("loop_lanes_disabled", `Loop lanes are disabled by the ${LANE_CONDUCT_LOOP_FLAG} kill switch`, {});
|
|
3339
|
+
}
|
|
3340
|
+
if (opts.loop === true && (opts.outpost?.trim() || requestedPlacement?.kind === "outpost" || defaultPlacement?.kind === "outpost")) {
|
|
3341
|
+
throw new LanesError("loop_lane_outpost_unsupported", "Loop lanes currently require the local host", { outpost_id: opts.outpost?.trim() ?? (requestedPlacement?.kind === "outpost" ? requestedPlacement.id : defaultPlacement?.kind === "outpost" ? defaultPlacement.id : undefined) });
|
|
3342
|
+
}
|
|
3326
3343
|
let placementCompanyResolved = false;
|
|
3327
3344
|
let placementCompany;
|
|
3328
3345
|
let placementGate;
|
|
@@ -3365,6 +3382,16 @@ async function runCreate(ctx, opts) {
|
|
|
3365
3382
|
const provider = explicitProvider ??
|
|
3366
3383
|
profile?.meta.provider ??
|
|
3367
3384
|
parseLaneProvider("codex");
|
|
3385
|
+
const continuityEnabled = opts.worker !== undefined && await readLanesRegistryFlagFailOpen(LANE_WORKER_CONTINUITY_FLAG, { company: company ?? meshCompany, env: ctx.env });
|
|
3386
|
+
let workerContinuity = continuityEnabled
|
|
3387
|
+
? findWorkerContinuityLane(loadAllLanes(hqRoot), {
|
|
3388
|
+
senior,
|
|
3389
|
+
worker: resolvedWorker?.id ?? opts.worker,
|
|
3390
|
+
provider,
|
|
3391
|
+
company: company ?? meshCompany,
|
|
3392
|
+
freshThread: opts.freshThread,
|
|
3393
|
+
})
|
|
3394
|
+
: { resumable: false };
|
|
3368
3395
|
const profileDefaultsApply = profile !== undefined && profile.meta.provider === provider;
|
|
3369
3396
|
const cliModel = parseCliModel(opts.model);
|
|
3370
3397
|
// Validated here because `claude --effort <unknown>` only warns and runs.
|
|
@@ -3375,6 +3402,9 @@ async function runCreate(ctx, opts) {
|
|
|
3375
3402
|
const profileAccounts = profileDefaultsApply ? profile?.meta.accounts : undefined;
|
|
3376
3403
|
const keepAlive = opts.keepAlive === true ||
|
|
3377
3404
|
(profileDefaultsApply && profile?.meta.keep_alive === true);
|
|
3405
|
+
if (opts.loop === true && keepAlive) {
|
|
3406
|
+
throw new LanesError("loop_keep_alive_conflict", "Loop lanes cannot use keep-alive", { worker: resolvedWorker?.id ?? opts.worker ?? null });
|
|
3407
|
+
}
|
|
3378
3408
|
if (keepAlive && !providerSupportsSessionResume(provider)) {
|
|
3379
3409
|
throw new LanesError("keep_alive_provider_unsupported", `Provider ${provider} cannot resume recorded sessions, so keep-alive is unavailable`, { provider });
|
|
3380
3410
|
}
|
|
@@ -3739,6 +3769,27 @@ async function runCreate(ctx, opts) {
|
|
|
3739
3769
|
// The lane id is created in `persist`; its Work Mesh thread is ensured
|
|
3740
3770
|
// immediately afterward from that stable identity.
|
|
3741
3771
|
const persist = async () => {
|
|
3772
|
+
const host = placementHostOverride ?? (outpostId !== undefined
|
|
3773
|
+
? { kind: "outpost", id: outpostId }
|
|
3774
|
+
: { kind: "local", id: ctx.hostname() });
|
|
3775
|
+
if (opts.loop === true && host.kind === "outpost") {
|
|
3776
|
+
throw new LanesError("loop_lane_outpost_unsupported", "Loop lanes currently require the local host", { outpost_id: host.id });
|
|
3777
|
+
}
|
|
3778
|
+
// Repeat the busy check while holding provider admission serialization so
|
|
3779
|
+
// two concurrent creates cannot both claim one worker's provider thread.
|
|
3780
|
+
if (continuityEnabled) {
|
|
3781
|
+
workerContinuity = findWorkerContinuityLane(loadAllLanes(hqRoot), {
|
|
3782
|
+
senior,
|
|
3783
|
+
worker: resolvedWorker?.id ?? opts.worker,
|
|
3784
|
+
provider,
|
|
3785
|
+
company: company ?? meshCompany,
|
|
3786
|
+
freshThread: opts.freshThread,
|
|
3787
|
+
});
|
|
3788
|
+
if (workerContinuity.resumable && workerContinuity.previous &&
|
|
3789
|
+
!sameLaneHost(workerContinuity.previous.host, host)) {
|
|
3790
|
+
workerContinuity = { previous: workerContinuity.previous, resumable: false, fallbackReason: "host_mismatch" };
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3742
3793
|
let admission;
|
|
3743
3794
|
let validation;
|
|
3744
3795
|
if (!outpostId && provider === "claude" && explicitBalanceAccount === undefined &&
|
|
@@ -3842,9 +3893,6 @@ async function runCreate(ctx, opts) {
|
|
|
3842
3893
|
throw admission.error;
|
|
3843
3894
|
}
|
|
3844
3895
|
}
|
|
3845
|
-
const host = placementHostOverride ?? (outpostId !== undefined
|
|
3846
|
-
? { kind: "outpost", id: outpostId }
|
|
3847
|
-
: { kind: "local", id: ctx.hostname() });
|
|
3848
3896
|
const queuedReason = admission.action === "queue" ? admission.reason : undefined;
|
|
3849
3897
|
const admissionReservedAt = admission.action === "start" ? ctx.now().toISOString() : undefined;
|
|
3850
3898
|
const laneInit = {
|
|
@@ -3886,8 +3934,21 @@ async function runCreate(ctx, opts) {
|
|
|
3886
3934
|
}
|
|
3887
3935
|
: {}),
|
|
3888
3936
|
keep_alive: keepAlive,
|
|
3937
|
+
...(opts.loop === true ? { loop_mode: true } : {}),
|
|
3889
3938
|
...(keepAlive ? { keep_alive_opted_in_at: createdAt } : {}),
|
|
3890
3939
|
provider,
|
|
3940
|
+
...(workerContinuity.resumable && workerContinuity.previous
|
|
3941
|
+
? {
|
|
3942
|
+
provider_session_id: workerContinuity.previous.provider_session_id,
|
|
3943
|
+
last_message_at: workerContinuity.previous.last_message_at ?? workerContinuity.previous.created_at,
|
|
3944
|
+
pending_resume: {
|
|
3945
|
+
session_id: workerContinuity.previous.provider_session_id,
|
|
3946
|
+
at: createdAt,
|
|
3947
|
+
trigger: "resume",
|
|
3948
|
+
resumed_from: workerContinuity.previous.state,
|
|
3949
|
+
},
|
|
3950
|
+
}
|
|
3951
|
+
: {}),
|
|
3891
3952
|
provider_source: explicitProvider !== undefined
|
|
3892
3953
|
? "cli"
|
|
3893
3954
|
: profile?.meta.provider !== undefined
|
|
@@ -3957,12 +4018,7 @@ async function runCreate(ctx, opts) {
|
|
|
3957
4018
|
return { lane, admission, validation };
|
|
3958
4019
|
};
|
|
3959
4020
|
let persisted;
|
|
3960
|
-
|
|
3961
|
-
persisted = await persist();
|
|
3962
|
-
}
|
|
3963
|
-
else {
|
|
3964
|
-
persisted = await withLaneAdmissionLock(hqRoot, provider, persist, ctx.sleep);
|
|
3965
|
-
}
|
|
4021
|
+
persisted = await ctx.withAdmissionLock(hqRoot, provider, persist, ctx.sleep);
|
|
3966
4022
|
let lane = persisted.lane;
|
|
3967
4023
|
const { admission, validation } = persisted;
|
|
3968
4024
|
if (focusAssessment.enabled && opts.focusOverride !== undefined) {
|
|
@@ -4052,10 +4108,25 @@ async function runCreate(ctx, opts) {
|
|
|
4052
4108
|
ensureDir(path.join(dest, "dropbox"));
|
|
4053
4109
|
ensureDir(path.join(dest, "questions"));
|
|
4054
4110
|
const taskBrief = fs.readFileSync(briefAbs, "utf8");
|
|
4055
|
-
const
|
|
4111
|
+
const previousResult = workerContinuity.previous
|
|
4112
|
+
? readLatestEnvelopeRecord(hqRoot, workerContinuity.previous.lane_id)
|
|
4113
|
+
: undefined;
|
|
4114
|
+
const briefText = `${workerContinuity.resumable && workerContinuity.previous
|
|
4115
|
+
? workerContinuityPrompt(workerContinuity.previous, previousResult?.envelope.summary, previousResult?.ref)
|
|
4116
|
+
: ""}${resolvedWorker
|
|
4056
4117
|
? composeWorkerBrief(resolvedWorker, taskBrief)
|
|
4057
|
-
: composeGeneralBrief(taskBrief)
|
|
4118
|
+
: composeGeneralBrief(taskBrief)}`;
|
|
4058
4119
|
fs.writeFileSync(path.join(dest, "brief.md"), briefText);
|
|
4120
|
+
if (workerContinuity.resumable && lane.pending_resume) {
|
|
4121
|
+
const newBriefPath = path.join(dest, "brief.md");
|
|
4122
|
+
lane = casUpdateLane(hqRoot, lane.lane_id, (current) => ({
|
|
4123
|
+
...current,
|
|
4124
|
+
pending_resume: current.pending_resume ? {
|
|
4125
|
+
...current.pending_resume,
|
|
4126
|
+
text: `The new brief at ${newBriefPath} is the instruction for this turn. Read and follow it.`,
|
|
4127
|
+
} : undefined,
|
|
4128
|
+
}));
|
|
4129
|
+
}
|
|
4059
4130
|
if (timeoutSecs !== undefined) {
|
|
4060
4131
|
writeJsonFile(timeoutPath(hqRoot, lane.lane_id), {
|
|
4061
4132
|
timeout_secs: timeoutSecs,
|
|
@@ -4063,7 +4134,21 @@ async function runCreate(ctx, opts) {
|
|
|
4063
4134
|
});
|
|
4064
4135
|
}
|
|
4065
4136
|
let started = lane;
|
|
4066
|
-
|
|
4137
|
+
let loopControllerPid;
|
|
4138
|
+
if (opts.loop === true) {
|
|
4139
|
+
lane = casUpdateLane(hqRoot, lane.lane_id, (current) => admission.action === "start"
|
|
4140
|
+
? {
|
|
4141
|
+
...current,
|
|
4142
|
+
state: "running",
|
|
4143
|
+
queued_reason: undefined,
|
|
4144
|
+
queued_details: undefined,
|
|
4145
|
+
}
|
|
4146
|
+
: { ...current });
|
|
4147
|
+
started = lane;
|
|
4148
|
+
if (admission.action === "start")
|
|
4149
|
+
loopControllerPid = startLaneLoopController(ctx, lane.lane_id, true);
|
|
4150
|
+
}
|
|
4151
|
+
else if (admission.action === "start") {
|
|
4067
4152
|
started = spawnLaneWorker(hqRoot, lane, {
|
|
4068
4153
|
env: ctx.env,
|
|
4069
4154
|
modelDeps: {
|
|
@@ -4119,9 +4204,20 @@ async function runCreate(ctx, opts) {
|
|
|
4119
4204
|
company: started.company,
|
|
4120
4205
|
project_id: started.project_id,
|
|
4121
4206
|
story_id: started.story_id,
|
|
4207
|
+
...(started.loop_mode ? { loop: loopStatus(runDir(hqRoot, started.lane_id), started.lane_id), loop_pid: loopControllerPid ?? null } : {}),
|
|
4122
4208
|
stories: laneStories(started),
|
|
4123
4209
|
story_warning: combinedStoryWarning(started, createStoryWarnings),
|
|
4124
4210
|
thread_id: started.thread_id ?? null,
|
|
4211
|
+
...(continuityEnabled ? {
|
|
4212
|
+
worker_continuity: workerContinuity.resumable
|
|
4213
|
+
? { action: "resume", previous_lane_id: workerContinuity.previous?.lane_id }
|
|
4214
|
+
: {
|
|
4215
|
+
action: "fresh",
|
|
4216
|
+
...(workerContinuity.previous ? { previous_lane_id: workerContinuity.previous.lane_id } : {}),
|
|
4217
|
+
...(opts.freshThread ? { reason: "fresh_thread_requested" } : {}),
|
|
4218
|
+
...(workerContinuity.fallbackReason ? { reason: workerContinuity.fallbackReason } : {}),
|
|
4219
|
+
},
|
|
4220
|
+
} : {}),
|
|
4125
4221
|
senior: started.senior,
|
|
4126
4222
|
depth: started.depth,
|
|
4127
4223
|
timeout_secs: timeoutSecs ?? null,
|
|
@@ -5343,6 +5439,24 @@ async function runPickup(ctx, opts) {
|
|
|
5343
5439
|
lane: started,
|
|
5344
5440
|
});
|
|
5345
5441
|
}
|
|
5442
|
+
async function laneStatusFieldsForCommand(ctx, hqRoot, lane, now) {
|
|
5443
|
+
const resolved = resolveLaneCompany(hqRoot, lane).company;
|
|
5444
|
+
let enabled = true;
|
|
5445
|
+
try {
|
|
5446
|
+
enabled = await readLanesRegistryFlagFailOpen(LANE_STATUS_ROW_FIELDS_FLAG, {
|
|
5447
|
+
...(resolved ? { company: resolved } : {}),
|
|
5448
|
+
env: ctx.env,
|
|
5449
|
+
});
|
|
5450
|
+
}
|
|
5451
|
+
catch (err) {
|
|
5452
|
+
process.stderr.write(`hq lanes: status row fields flag read failed open: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
5453
|
+
}
|
|
5454
|
+
if (!enabled)
|
|
5455
|
+
return {};
|
|
5456
|
+
const host = reconcileHostFromCtx(ctx);
|
|
5457
|
+
const local = laneRunsHere(lane, host);
|
|
5458
|
+
return { ...readLaneStatusRowFields(hqRoot, lane, now.getTime(), local) };
|
|
5459
|
+
}
|
|
5346
5460
|
async function runList(ctx, opts) {
|
|
5347
5461
|
if (opts.json && opts.human) {
|
|
5348
5462
|
throw new LanesError("conflicting_output_format", "Use either --json or --human for hq lanes list", { json: true, human: true });
|
|
@@ -5365,7 +5479,7 @@ async function runList(ctx, opts) {
|
|
|
5365
5479
|
new Set(opts.hierarchy).size !== opts.hierarchy.length)) {
|
|
5366
5480
|
throw new LanesError("lane_list_hierarchy_invalid", "--hierarchy must be repeated with distinct, non-empty lane ids without surrounding whitespace", {});
|
|
5367
5481
|
}
|
|
5368
|
-
if (opts.snapshot && (opts.human || opts.company || opts.project || opts.story || opts.state || opts.senior)) {
|
|
5482
|
+
if (opts.snapshot && (opts.human || opts.company || opts.project || opts.story || opts.state || opts.senior || opts.worker)) {
|
|
5369
5483
|
throw new LanesError("lane_list_snapshot_invalid", "--snapshot only supports owner lane filters and JSON output", {});
|
|
5370
5484
|
}
|
|
5371
5485
|
if (opts.hierarchy !== undefined && !opts.snapshot) {
|
|
@@ -5414,21 +5528,30 @@ async function runList(ctx, opts) {
|
|
|
5414
5528
|
}
|
|
5415
5529
|
if (senior && seniorKey(lane.senior) !== seniorKey(senior))
|
|
5416
5530
|
return false;
|
|
5531
|
+
if (opts.worker && lane.worker !== opts.worker)
|
|
5532
|
+
return false;
|
|
5417
5533
|
return true;
|
|
5418
5534
|
};
|
|
5419
5535
|
const selectLanes = () => laneSource
|
|
5420
5536
|
.filter((lane) => matchesLaneFilters(lane, true))
|
|
5421
5537
|
.sort(compareLaneQueueOrder);
|
|
5422
5538
|
let selectedLanes = selectLanes();
|
|
5539
|
+
const statusFieldsByLane = new Map();
|
|
5540
|
+
const statusNow = ctx.now();
|
|
5541
|
+
await Promise.all(selectedLanes.map(async (lane) => {
|
|
5542
|
+
statusFieldsByLane.set(lane.lane_id, await laneStatusFieldsForCommand(ctx, hqRoot, lane, statusNow));
|
|
5543
|
+
}));
|
|
5423
5544
|
if (opts.snapshot) {
|
|
5424
5545
|
printJson({
|
|
5425
5546
|
ok: true,
|
|
5426
5547
|
action: "list",
|
|
5427
5548
|
lanes: selectedLanes.map((lane) => {
|
|
5549
|
+
const fields = statusFieldsByLane.get(lane.lane_id) ?? {};
|
|
5550
|
+
const loop = lane.loop_mode === true ? { loop: loopStatus(runDir(hqRoot, lane.lane_id), lane.lane_id) } : {};
|
|
5428
5551
|
if (!isTerminalLaneState(lane.state))
|
|
5429
|
-
return lane;
|
|
5552
|
+
return { ...lane, ...fields, ...loop };
|
|
5430
5553
|
const envelope = readLatestEnvelopeRecord(hqRoot, lane.lane_id)?.envelope;
|
|
5431
|
-
return envelope?.summary ? {
|
|
5554
|
+
return { ...lane, ...fields, ...loop, ...(envelope?.summary ? { envelope_summary: envelope.summary } : {}) };
|
|
5432
5555
|
}),
|
|
5433
5556
|
});
|
|
5434
5557
|
return;
|
|
@@ -5532,7 +5655,9 @@ async function runList(ctx, opts) {
|
|
|
5532
5655
|
!laneRunsHere(lane, host);
|
|
5533
5656
|
return {
|
|
5534
5657
|
...lane,
|
|
5658
|
+
...statusFieldsByLane.get(lane.lane_id),
|
|
5535
5659
|
...hireByLane.get(lane.lane_id),
|
|
5660
|
+
...(lane.loop_mode === true ? { loop: loopStatus(runDir(hqRoot, lane.lane_id), lane.lane_id) } : {}),
|
|
5536
5661
|
...(remoteRefresh.unverifiedRemote.has(lane.lane_id) ? { unverified_remote: true } : {}),
|
|
5537
5662
|
stories: laneStories(lane),
|
|
5538
5663
|
story_ids: laneStories(lane).map((story) => story.story_id),
|
|
@@ -5901,7 +6026,11 @@ async function runShow(ctx, laneId, hasPendingQuestionForLane, resolveLaneTarget
|
|
|
5901
6026
|
})
|
|
5902
6027
|
.slice(0, 20)
|
|
5903
6028
|
.map(({ repo, pr, head_sha, recorded_at }) => ({ repo, pr, head_sha, recorded_at }));
|
|
5904
|
-
const laneForShow = {
|
|
6029
|
+
const laneForShow = {
|
|
6030
|
+
...lane,
|
|
6031
|
+
...await laneStatusFieldsForCommand(ctx, hqRoot, storedLane, ctx.now()),
|
|
6032
|
+
...(storedLane.loop_mode === true ? { loop: loopStatus(runDir(hqRoot, laneId), laneId) } : {}),
|
|
6033
|
+
};
|
|
5905
6034
|
delete laneForShow.resume_history;
|
|
5906
6035
|
printJson({
|
|
5907
6036
|
ok: true,
|
|
@@ -7695,6 +7824,7 @@ async function runReparentLocked(ctx, laneId, opts, finishedChildrenDoNotCount =
|
|
|
7695
7824
|
}
|
|
7696
7825
|
assertLaneNotRunningForReparent(lane);
|
|
7697
7826
|
const pending = pendingQuestionsForLane(hqRoot, laneId);
|
|
7827
|
+
const pendingAudiences = new Map(pending.map((question) => [question.question_id, resolveAudience(hqRoot, question, ctx)]));
|
|
7698
7828
|
const oldDepth = derivedLaneDepth(hqRoot, lane.senior, ctx);
|
|
7699
7829
|
if (newDepth > oldDepth && pending.length > 0) {
|
|
7700
7830
|
throw new LanesError("demotion_with_pending_questions", "Cannot increase a lane's depth while it has pending questions", { lane_id: laneId, from_depth: oldDepth, to_depth: newDepth });
|
|
@@ -7759,17 +7889,24 @@ async function runReparentLocked(ctx, laneId, opts, finishedChildrenDoNotCount =
|
|
|
7759
7889
|
}
|
|
7760
7890
|
for (const question of pending) {
|
|
7761
7891
|
casUpdateQuestion(hqRoot, question.question_id, (current) => {
|
|
7892
|
+
const previousAudience = pendingAudiences.get(current.question_id);
|
|
7893
|
+
const nextAudience = resolveAudience(hqRoot, current, ctx);
|
|
7894
|
+
const audienceChanged = previousAudience !== undefined &&
|
|
7895
|
+
seniorKey(previousAudience) !== seniorKey(nextAudience);
|
|
7762
7896
|
const already = current.routing_history.some((entry) => entry.reason === "reparented" && entry.at === at);
|
|
7763
|
-
if (already)
|
|
7897
|
+
if (already && !audienceChanged)
|
|
7764
7898
|
return current;
|
|
7765
7899
|
return {
|
|
7766
7900
|
...current,
|
|
7767
|
-
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
|
|
7901
|
+
...(audienceChanged ? { seen_at: null, seen_session_id: null } : {}),
|
|
7902
|
+
routing_history: already
|
|
7903
|
+
? current.routing_history
|
|
7904
|
+
: appendCappedHistory(current.routing_history, {
|
|
7905
|
+
hop: current.hop,
|
|
7906
|
+
audience: nextAudience,
|
|
7907
|
+
at,
|
|
7908
|
+
reason: "reparented",
|
|
7909
|
+
}),
|
|
7773
7910
|
};
|
|
7774
7911
|
});
|
|
7775
7912
|
}
|
|
@@ -8380,7 +8517,7 @@ async function runStop(ctx, laneId, reason, quiet = false) {
|
|
|
8380
8517
|
const hqRoot = ctx.hqRoot();
|
|
8381
8518
|
const target = await resolveCommandLaneTarget(ctx, laneId);
|
|
8382
8519
|
const lane = target.lane;
|
|
8383
|
-
if (!quiet && lane.state !== "queued" && !isTerminalLaneState(lane.state)) {
|
|
8520
|
+
if (!quiet && lane.loop_mode !== true && lane.state !== "queued" && !isTerminalLaneState(lane.state)) {
|
|
8384
8521
|
process.stderr.write(stopTerminalWarning(lane.lane_id));
|
|
8385
8522
|
}
|
|
8386
8523
|
if (target.remoteOnly) {
|
|
@@ -8392,6 +8529,23 @@ async function runStop(ctx, laneId, reason, quiet = false) {
|
|
|
8392
8529
|
return;
|
|
8393
8530
|
}
|
|
8394
8531
|
assertLaneRunsHereForOperatorMutation(ctx, lane, "stop");
|
|
8532
|
+
if (lane.loop_mode === true) {
|
|
8533
|
+
const runDirPath = runDir(hqRoot, lane.lane_id);
|
|
8534
|
+
const loop = loopStatus(runDirPath, lane.lane_id);
|
|
8535
|
+
if (loop.state === "stopped" || loop.state === "failed") {
|
|
8536
|
+
throw new LanesError("already_terminal", `Loop lane ${lane.lane_id} is already ${lane.state}`, { lane_id: lane.lane_id, state: lane.state });
|
|
8537
|
+
}
|
|
8538
|
+
const controllerAlive = loopControllerIsAlive(runDirPath);
|
|
8539
|
+
if (!controllerAlive && lane.state === "running") {
|
|
8540
|
+
await runInterrupt(ctx, lane.lane_id, "loop controller exited; recovering queued work before stop", { preserveLoopQueue: true, quiet: true });
|
|
8541
|
+
}
|
|
8542
|
+
const queued = enqueueLoopEnvelope({ runDir: runDirPath, laneId: lane.lane_id, envelope: { kind: "stop" }, now: ctx.now });
|
|
8543
|
+
if (!controllerAlive && lane.state !== "queued")
|
|
8544
|
+
startLaneLoopController(ctx, lane.lane_id);
|
|
8545
|
+
if (!quiet)
|
|
8546
|
+
printJson({ ok: true, action: "stop", lane_id: lane.lane_id, terminal: false, queued_stop: queued });
|
|
8547
|
+
return;
|
|
8548
|
+
}
|
|
8395
8549
|
if (isTerminalLaneState(lane.state)) {
|
|
8396
8550
|
throw new LanesError("already_terminal", `Lane ${laneId} is already ${lane.state}`, { lane_id: laneId, state: lane.state });
|
|
8397
8551
|
}
|
|
@@ -8447,7 +8601,7 @@ function stopTerminalWarning(laneId) {
|
|
|
8447
8601
|
* session so `hq lanes resume` continues it. Keep-alive resumes only
|
|
8448
8602
|
* `exited_with_partial_output` interruptions, so it leaves this lane alone.
|
|
8449
8603
|
*/
|
|
8450
|
-
async function runInterrupt(ctx, laneId, reason) {
|
|
8604
|
+
async function runInterrupt(ctx, laneId, reason, options = {}) {
|
|
8451
8605
|
const operatorReason = reason?.trim();
|
|
8452
8606
|
if (reason !== undefined && !operatorReason) {
|
|
8453
8607
|
throw new LanesError("lane_interrupt_reason_invalid", "Interrupt reason must not be empty", {});
|
|
@@ -8460,6 +8614,14 @@ async function runInterrupt(ctx, laneId, reason) {
|
|
|
8460
8614
|
throw new LanesError("interrupt_remote_unsupported", `Lane ${laneId} runs on Outpost ${lane.host.id}; run hq lanes interrupt on that Outpost`, { lane_id: laneId, outpost_id: lane.host.id });
|
|
8461
8615
|
}
|
|
8462
8616
|
assertLaneRunsHereForOperatorMutation(ctx, lane, "interrupt");
|
|
8617
|
+
const queueInterrupt = lane.loop_mode === true && options.preserveLoopQueue !== true
|
|
8618
|
+
? interruptLoopQueue(runDir(hqRoot, lane.lane_id))
|
|
8619
|
+
: undefined;
|
|
8620
|
+
if (lane.loop_mode === true && lane.state !== "running") {
|
|
8621
|
+
if (!options.quiet)
|
|
8622
|
+
printJson({ ok: true, action: "interrupt", lane_id: lane.lane_id, terminal: false, resumable: true, ...queueInterrupt });
|
|
8623
|
+
return;
|
|
8624
|
+
}
|
|
8463
8625
|
const assertInterruptible = (current) => {
|
|
8464
8626
|
if (current.state !== "running") {
|
|
8465
8627
|
throw new LanesError("lane_not_running", current.state === "queued"
|
|
@@ -8502,13 +8664,15 @@ async function runInterrupt(ctx, laneId, reason) {
|
|
|
8502
8664
|
const resumable = resumeCommandRefusal(laneWithRecoveredProviderSession(hqRoot, updated)) === null;
|
|
8503
8665
|
if (!resumable)
|
|
8504
8666
|
process.stderr.write(interruptNotResumableWarning(laneId));
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8667
|
+
if (!options.quiet)
|
|
8668
|
+
printJson({
|
|
8669
|
+
ok: true,
|
|
8670
|
+
action: "interrupt",
|
|
8671
|
+
lane: updated,
|
|
8672
|
+
terminal: false,
|
|
8673
|
+
resumable,
|
|
8674
|
+
...(queueInterrupt === undefined ? {} : queueInterrupt),
|
|
8675
|
+
});
|
|
8512
8676
|
}
|
|
8513
8677
|
/**
|
|
8514
8678
|
* The watcher skips lanes that are already terminal, so it never records the
|
|
@@ -8672,13 +8836,13 @@ function questionListCompanyArg(company) {
|
|
|
8672
8836
|
}
|
|
8673
8837
|
return value;
|
|
8674
8838
|
}
|
|
8675
|
-
async function readOutpostQuestionRecords(ctx, outpostId, company) {
|
|
8839
|
+
async function readOutpostQuestionRecords(ctx, outpostId, company, questionId, visibilityFilter) {
|
|
8676
8840
|
const companyArg = questionListCompanyArg(company);
|
|
8677
8841
|
let result;
|
|
8678
8842
|
try {
|
|
8679
8843
|
result = await ctx.dispatchOutpostExec({
|
|
8680
8844
|
outpostId,
|
|
8681
|
-
command: buildOutpostQuestionListCommand({ company: companyArg }),
|
|
8845
|
+
command: buildOutpostQuestionListCommand({ company: companyArg, ...visibilityFilter }),
|
|
8682
8846
|
timeoutSeconds: CAPACITY_PROBE_TIMEOUT_MS / 1000,
|
|
8683
8847
|
waitMs: CAPACITY_PROBE_TIMEOUT_MS,
|
|
8684
8848
|
});
|
|
@@ -8711,6 +8875,8 @@ async function readOutpostQuestionRecords(ctx, outpostId, company) {
|
|
|
8711
8875
|
throw new LanesError("outpost_question_list_invalid", `Outpost ${outpostId} returned an invalid question record`, { outpost_id: outpostId, index });
|
|
8712
8876
|
}
|
|
8713
8877
|
const rowObject = row;
|
|
8878
|
+
if (questionId !== undefined && rowObject.question_id !== questionId)
|
|
8879
|
+
continue;
|
|
8714
8880
|
let question;
|
|
8715
8881
|
try {
|
|
8716
8882
|
question = parseQuestionRecord(row);
|
|
@@ -8840,6 +9006,31 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
|
|
|
8840
9006
|
runCompaction: ctx.runCompaction,
|
|
8841
9007
|
locateTranscript: ctx.locateTranscript,
|
|
8842
9008
|
},
|
|
9009
|
+
spawn: (root, candidate) => {
|
|
9010
|
+
if (candidate.loop_mode !== true || candidate.state !== "queued") {
|
|
9011
|
+
return spawnLaneWorker(root, candidate, {
|
|
9012
|
+
env: ctx.env,
|
|
9013
|
+
modelDeps: { readFileSync: ctx.readFileSync, probe: ctx.probeProvider, env: ctx.env },
|
|
9014
|
+
existsSync: ctx.existsSync,
|
|
9015
|
+
spawnProcess: ctx.spawnProcess,
|
|
9016
|
+
proveDetached: ctx.proveDetached,
|
|
9017
|
+
killProcess: ctx.killProcess,
|
|
9018
|
+
now: ctx.now,
|
|
9019
|
+
resolveMachineIdentity: ctx.resolveMachineIdentity,
|
|
9020
|
+
runCompaction: ctx.runCompaction,
|
|
9021
|
+
locateTranscript: ctx.locateTranscript,
|
|
9022
|
+
});
|
|
9023
|
+
}
|
|
9024
|
+
const admitted = casUpdateLane(root, candidate.lane_id, (current) => ({
|
|
9025
|
+
...current,
|
|
9026
|
+
state: "running",
|
|
9027
|
+
queued_reason: undefined,
|
|
9028
|
+
queued_details: undefined,
|
|
9029
|
+
admission_reserved_at: undefined,
|
|
9030
|
+
}));
|
|
9031
|
+
startLaneLoopController(ctx, candidate.lane_id, true);
|
|
9032
|
+
return admitted;
|
|
9033
|
+
},
|
|
8843
9034
|
throttleDeps: ctx.throttleDeps,
|
|
8844
9035
|
livenessDeps: livenessDepsFromCtx(ctx),
|
|
8845
9036
|
presenceConnected: ctx.presenceConnected,
|
|
@@ -8847,6 +9038,63 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
|
|
|
8847
9038
|
recoverQueuedMessages,
|
|
8848
9039
|
mesh: ctx.mesh,
|
|
8849
9040
|
env: ctx.env,
|
|
9041
|
+
deliverBlockedEscalation: async (delivery) => {
|
|
9042
|
+
const currentHost = reconcileHostFromCtx(ctx);
|
|
9043
|
+
const targetOutpostId = delivery.recipient_host?.kind === "outpost"
|
|
9044
|
+
? delivery.recipient_host.id
|
|
9045
|
+
: "primary";
|
|
9046
|
+
const targetIsCurrent = targetOutpostId === (currentHost.outpostId ?? "primary");
|
|
9047
|
+
if (targetIsCurrent)
|
|
9048
|
+
throw new LanesError("blocked_escalation_local_delivery_misrouted", "Local blocked escalations must use the durable local inbox path");
|
|
9049
|
+
if (!delivery.recipient.id)
|
|
9050
|
+
throw new LanesError("blocked_escalation_recipient_invalid", "Escalation recipient has no id");
|
|
9051
|
+
const waitMs = outpostSingleLaneWaitMs(ctx.env);
|
|
9052
|
+
const dispatched = await ctx.dispatchOutpostExec({
|
|
9053
|
+
outpostId: targetOutpostId,
|
|
9054
|
+
timeoutSeconds: Math.ceil(waitMs / 1000),
|
|
9055
|
+
waitMs,
|
|
9056
|
+
command: buildOutpostLaneCommand({
|
|
9057
|
+
action: "blocked-escalation-target",
|
|
9058
|
+
laneId: delivery.recipient.id,
|
|
9059
|
+
recipientKind: delivery.recipient.kind === "session" ? "session" : "lane",
|
|
9060
|
+
text: delivery.text,
|
|
9061
|
+
messageId: delivery.message_id,
|
|
9062
|
+
}),
|
|
9063
|
+
});
|
|
9064
|
+
if (dispatched.exitCode !== 0)
|
|
9065
|
+
throw new LanesError("blocked_escalation_remote_delivery_failed", dispatchErrorMessage(dispatched), { outpost_id: targetOutpostId });
|
|
9066
|
+
},
|
|
9067
|
+
resolveEscalationLane: async (laneId) => {
|
|
9068
|
+
const currentHost = reconcileHostFromCtx(ctx);
|
|
9069
|
+
if (currentHost.outpostId === null)
|
|
9070
|
+
return null;
|
|
9071
|
+
const waitMs = outpostSingleLaneWaitMs(ctx.env);
|
|
9072
|
+
const dispatched = await ctx.dispatchOutpostExec({
|
|
9073
|
+
outpostId: "primary",
|
|
9074
|
+
command: buildOutpostLaneCommand({ action: "show", laneId }),
|
|
9075
|
+
timeoutSeconds: Math.ceil(waitMs / 1000),
|
|
9076
|
+
waitMs,
|
|
9077
|
+
});
|
|
9078
|
+
if (dispatched.done !== true || dispatched.status !== "Success" || dispatched.exitCode !== 0) {
|
|
9079
|
+
throw new LanesError("blocked_escalation_ancestor_unavailable", `Could not read ancestor lane ${laneId} from the primary hierarchy`, {
|
|
9080
|
+
lane_id: laneId,
|
|
9081
|
+
command_id: dispatched.commandId,
|
|
9082
|
+
exit_code: dispatched.exitCode,
|
|
9083
|
+
});
|
|
9084
|
+
}
|
|
9085
|
+
const remote = parseRemoteJson(dispatched.stdout, "Blocked escalation ancestor");
|
|
9086
|
+
if (remote?.ok !== true || remote.action !== "show" || remote.lane === undefined) {
|
|
9087
|
+
throw new LanesError("blocked_escalation_ancestor_invalid", `Primary did not return ancestor lane ${laneId}`, { lane_id: laneId });
|
|
9088
|
+
}
|
|
9089
|
+
const ancestor = parseLaneRecord(normalizeLaneRecordView(remote.lane));
|
|
9090
|
+
if (ancestor.lane_id !== laneId) {
|
|
9091
|
+
throw new LanesError("blocked_escalation_ancestor_invalid", `Primary returned a different lane for ancestor lookup ${laneId}`, {
|
|
9092
|
+
requested_lane_id: laneId,
|
|
9093
|
+
returned_lane_id: ancestor.lane_id,
|
|
9094
|
+
});
|
|
9095
|
+
}
|
|
9096
|
+
return ancestor;
|
|
9097
|
+
},
|
|
8850
9098
|
...(opts.terminalKeepAliveCandidate !== undefined
|
|
8851
9099
|
? { terminalKeepAliveCandidate: opts.terminalKeepAliveCandidate }
|
|
8852
9100
|
: {}),
|
|
@@ -9248,6 +9496,15 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9248
9496
|
presenceConnected: ctx.presenceConnected ?? false,
|
|
9249
9497
|
deps: ctx.livenessDeps,
|
|
9250
9498
|
}),
|
|
9499
|
+
stopEnabled: (lane) => {
|
|
9500
|
+
const resolution = resolveLaneCompany(ctx.hqRoot(), lane);
|
|
9501
|
+
const company = resolution.company ?? undefined;
|
|
9502
|
+
return readLanesRegistryFlag(LANE_REAP_STOP_FLAG, {
|
|
9503
|
+
company,
|
|
9504
|
+
...(company ? { companyUidSource: laneCompanyUidSource(resolution.source) } : {}),
|
|
9505
|
+
env: ctx.env,
|
|
9506
|
+
});
|
|
9507
|
+
},
|
|
9251
9508
|
}, wrap);
|
|
9252
9509
|
registerLaneDropboxRepairCommand(lanes, {
|
|
9253
9510
|
hqRoot: ctx.hqRoot,
|
|
@@ -9313,6 +9570,8 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9313
9570
|
.option("--additional-story <id>", "Additional story carried by this lane; repeatable and discouraged when active", collectRepeatable)
|
|
9314
9571
|
.requiredOption("--brief-file <path>", "Path to the lane brief")
|
|
9315
9572
|
.option("--worker <name>", "HQ worker or reusable profile id")
|
|
9573
|
+
.option("--fresh-thread", "Skip worker continuity and start a cold provider session")
|
|
9574
|
+
.option("--loop", "Keep one worker lane registered to process queued phase envelopes")
|
|
9316
9575
|
.option("--provider <name>", "claude | codex | grok")
|
|
9317
9576
|
.option("--account <id>", "Provider account id")
|
|
9318
9577
|
.option("--model <id>", "Model id for the provider CLI")
|
|
@@ -9335,6 +9594,62 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9335
9594
|
.action(wrap(async (laneId, opts) => {
|
|
9336
9595
|
await runApproveOwnerTarget(ctx, laneId, opts);
|
|
9337
9596
|
}));
|
|
9597
|
+
lanes
|
|
9598
|
+
.command("_loop-worker", { hidden: true })
|
|
9599
|
+
.argument("<lane-id>", "Loop lane id")
|
|
9600
|
+
.action(wrap(async (laneId) => {
|
|
9601
|
+
await runLaneLoopController(ctx, laneId, {
|
|
9602
|
+
resumeLane: (id, options) => resumeHandlers.runResume(ctx, id, options),
|
|
9603
|
+
interruptLane: (id, reason, options) => runInterrupt(ctx, id, reason, options),
|
|
9604
|
+
emitTerminal: (lane) => emitLaneTerminalStateSoft(ctx, lane),
|
|
9605
|
+
});
|
|
9606
|
+
}));
|
|
9607
|
+
lanes
|
|
9608
|
+
.command("enqueue")
|
|
9609
|
+
.description("Validate and queue one phase envelope for a loop lane")
|
|
9610
|
+
.argument("<lane-id>", "Loop lane id")
|
|
9611
|
+
.requiredOption("--envelope <file>", "Generic phase or hq-phase-envelope/v1 JSON file")
|
|
9612
|
+
.action(wrap(async (laneId, opts) => {
|
|
9613
|
+
const hqRoot = ctx.hqRoot();
|
|
9614
|
+
const lane = readLane(hqRoot, laneId);
|
|
9615
|
+
if (lane.loop_mode !== true)
|
|
9616
|
+
throw new LanesError("lane_not_loop_mode", `Lane ${laneId} is not a loop lane`, { lane_id: laneId });
|
|
9617
|
+
const authorization = callerSeniorAuthorization(ctx, lane.senior);
|
|
9618
|
+
if (!authorization.authorized)
|
|
9619
|
+
throw new LanesError("lane_enqueue_unauthorized", `Only the lane's senior may enqueue phases`, { lane_id: laneId, reason: authorization.reason });
|
|
9620
|
+
assertLaneRunsHereForAdmission(ctx, lane);
|
|
9621
|
+
const enabled = await readLanesRegistryFlagFailOpen(LANE_CONDUCT_LOOP_FLAG, { company: lane.company ?? lane.mesh_company, defaultValue: true, env: ctx.env });
|
|
9622
|
+
if (!enabled)
|
|
9623
|
+
throw new LanesError("loop_lanes_disabled", `Loop lanes are disabled by the ${LANE_CONDUCT_LOOP_FLAG} kill switch`, { lane_id: laneId });
|
|
9624
|
+
const source = resolveCallerPath(ctx.callerCwd(), opts.envelope ?? "");
|
|
9625
|
+
let envelope;
|
|
9626
|
+
try {
|
|
9627
|
+
envelope = validateLoopEnvelope(JSON.parse(fs.readFileSync(source, "utf8")));
|
|
9628
|
+
}
|
|
9629
|
+
catch (error) {
|
|
9630
|
+
throw new LanesError("loop_envelope_invalid", `Could not read a valid JSON envelope from ${source}: ${error instanceof Error ? error.message : String(error)}`, { lane_id: laneId, path: source });
|
|
9631
|
+
}
|
|
9632
|
+
const envelopeData = envelope;
|
|
9633
|
+
if (envelopeData.engine !== undefined && envelopeData.engine !== lane.provider) {
|
|
9634
|
+
throw new LanesError("loop_envelope_engine_mismatch", `Envelope engine ${String(envelopeData.engine)} does not match lane provider ${lane.provider}`, { lane_id: lane.lane_id, provider: lane.provider });
|
|
9635
|
+
}
|
|
9636
|
+
if (envelopeData.schema === "hq-phase-envelope/v1" && envelopeData.worker_id !== lane.worker) {
|
|
9637
|
+
throw new LanesError("loop_envelope_worker_mismatch", `Envelope worker ${String(envelopeData.worker_id)} does not match lane worker ${lane.worker ?? "<none>"}`, { lane_id: lane.lane_id, worker: lane.worker ?? null });
|
|
9638
|
+
}
|
|
9639
|
+
const runDirPath = runDir(hqRoot, lane.lane_id);
|
|
9640
|
+
const loop = loopStatus(runDirPath, lane.lane_id);
|
|
9641
|
+
if (loop.state === "stopped" || loop.state === "failed") {
|
|
9642
|
+
throw new LanesError("loop_not_running", `Loop lane ${lane.lane_id} is ${loop.state}`, { lane_id: lane.lane_id, loop_state: loop.state });
|
|
9643
|
+
}
|
|
9644
|
+
const controllerAlive = loopControllerIsAlive(runDirPath);
|
|
9645
|
+
const result = enqueueLoopEnvelope({ runDir: runDirPath, laneId: lane.lane_id, envelope, now: ctx.now });
|
|
9646
|
+
if (!controllerAlive && lane.state === "running") {
|
|
9647
|
+
await runInterrupt(ctx, lane.lane_id, "loop controller exited; recovering the queued phase", { preserveLoopQueue: true, quiet: true });
|
|
9648
|
+
}
|
|
9649
|
+
const shouldRestart = !controllerAlive && lane.state !== "queued";
|
|
9650
|
+
const controllerPid = shouldRestart ? startLaneLoopController(ctx, lane.lane_id) : undefined;
|
|
9651
|
+
printJson({ ok: true, action: "enqueue", ...result, lane: lane.lane_id, ...(controllerPid ? { controller_pid: controllerPid } : {}) });
|
|
9652
|
+
}));
|
|
9338
9653
|
lanes
|
|
9339
9654
|
.command("reparent-target", { hidden: true })
|
|
9340
9655
|
.argument("<lane-id>", "Lane id")
|
|
@@ -9412,6 +9727,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9412
9727
|
.option("--story <id>", "Filter by story")
|
|
9413
9728
|
.option("--state <state>", `Filter by state (${LANE_STATES.join("|")})`)
|
|
9414
9729
|
.option("--senior <ref>", "Filter by senior ref")
|
|
9730
|
+
.option("--worker <id>", "Filter by worker id")
|
|
9415
9731
|
.option("--lane <id>", "Filter by lane id (repeatable)", (value, previous = []) => [...previous, value], [])
|
|
9416
9732
|
.addOption(new Option("--lane-ulid <ulid>", "Filter by lane ULID for internal owner lookup").hideHelp()
|
|
9417
9733
|
.argParser((value, previous = []) => [...previous, value]))
|
|
@@ -9600,6 +9916,31 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9600
9916
|
.action(wrap(async (laneId) => {
|
|
9601
9917
|
printJson(await messageHandlers.recoverQueuedMessages(ctx, laneId));
|
|
9602
9918
|
}));
|
|
9919
|
+
lanes
|
|
9920
|
+
.command("_enqueue-blocked-escalation <recipient-kind> <recipient-id>", { hidden: true })
|
|
9921
|
+
.option("--text <text>", "Escalation notice")
|
|
9922
|
+
.option("--message-id <id>", "Idempotency key")
|
|
9923
|
+
.action(wrap(async (recipientKind, recipientId, opts) => {
|
|
9924
|
+
if (!ctx.isSsmOwnerDispatch())
|
|
9925
|
+
throw new LanesError("blocked_escalation_dispatch_forbidden", "Blocked escalation inbox writes require owner dispatch");
|
|
9926
|
+
if (recipientKind !== "lane" && recipientKind !== "session")
|
|
9927
|
+
throw new LanesError("blocked_escalation_recipient_invalid", `Unknown recipient kind ${recipientKind}`);
|
|
9928
|
+
if (!opts.text?.trim() || !opts.messageId?.trim())
|
|
9929
|
+
throw new LanesError("blocked_escalation_payload_invalid", "Escalation inbox write requires text and message id");
|
|
9930
|
+
const message = { at: ctx.now().toISOString(), message_id: opts.messageId, kind: "blocked_escalation", text: opts.text };
|
|
9931
|
+
if (recipientKind === "lane") {
|
|
9932
|
+
appendDropboxMessageOnce(ctx.hqRoot(), recipientId, message);
|
|
9933
|
+
}
|
|
9934
|
+
else {
|
|
9935
|
+
const dirs = ["claude", "codex", "grok"]
|
|
9936
|
+
.map((provider) => monitorSessionDir(ctx.hqRoot(), provider, recipientId))
|
|
9937
|
+
.filter((dir) => ctx.existsSync(dir));
|
|
9938
|
+
if (dirs.length !== 1)
|
|
9939
|
+
throw new LanesError("blocked_escalation_recipient_unavailable", `Expected one durable inbox for session ${recipientId}; found ${dirs.length}`);
|
|
9940
|
+
appendDropboxMessageOnceAtPath(path.join(dirs[0], "dropbox"), message);
|
|
9941
|
+
}
|
|
9942
|
+
printJson({ ok: true, recipient_kind: recipientKind, recipient_id: recipientId, message_id: opts.messageId });
|
|
9943
|
+
}));
|
|
9603
9944
|
lanes
|
|
9604
9945
|
.command("_spawn-envelope-repair", { hidden: true })
|
|
9605
9946
|
.action(wrap(async () => {
|