@indigoai-us/hq-cli 5.293.0 → 5.295.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 +39 -0
- package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +5 -1
- package/dist/command-catalog.generated.d.ts +54 -33
- package/dist/command-catalog.generated.js +68 -42
- package/dist/commands/auth.d.ts +1 -0
- package/dist/commands/auth.js +76 -1
- package/dist/commands/core-command-catalog.js +3 -2
- package/dist/commands/core-delegate-w1b.d.ts +2 -0
- package/dist/commands/core-delegate-w1b.js +28 -2
- package/dist/commands/core-delegate.d.ts +3 -1
- package/dist/commands/core-delegate.js +35 -5
- package/dist/commands/core-handoff-fast.d.ts +18 -0
- package/dist/commands/core-handoff-fast.js +103 -0
- package/dist/commands/core-handoff-open-steps.d.ts +21 -0
- package/dist/commands/core-handoff-open-steps.js +264 -0
- package/dist/commands/core-handoff.d.ts +4 -2
- package/dist/commands/core-handoff.js +22 -249
- package/dist/commands/core-jobs-native.d.ts +2 -0
- package/dist/commands/core-jobs-native.js +22 -11
- package/dist/commands/core-knowledge-prefs.d.ts +2 -0
- package/dist/commands/core-knowledge-prefs.js +13 -1
- package/dist/commands/core-ontology-native.d.ts +2 -0
- package/dist/commands/core-ontology-native.js +22 -2
- package/dist/commands/core-policy.d.ts +2 -0
- package/dist/commands/core-policy.js +18 -1
- package/dist/commands/core.js +2 -1
- package/dist/commands/daemon.d.ts +2 -1
- package/dist/commands/daemon.js +22 -7
- package/dist/commands/doctor.js +0 -74
- package/dist/commands/lanes-autopilot.d.ts +11 -0
- package/dist/commands/lanes-autopilot.js +336 -0
- package/dist/commands/lanes-message.js +23 -5
- package/dist/commands/lanes-questions.js +8 -5
- package/dist/commands/lanes-resume.js +0 -3
- package/dist/commands/lanes-shared.d.ts +18 -2
- package/dist/commands/lanes-stories.d.ts +14 -0
- package/dist/commands/lanes-stories.js +308 -0
- package/dist/commands/lanes.d.ts +1 -9
- package/dist/commands/lanes.js +216 -481
- package/dist/fast-core.js +53 -25
- package/dist/fast-scaffold.d.ts +15 -7
- package/dist/fast-scaffold.js +167 -17
- package/dist/index.js +1 -10
- package/dist/lib/core-utils/sentry-report.d.ts +0 -19
- package/dist/lib/core-utils/sentry-report.js +8 -61
- package/dist/lib/core-utils/worktree-resume.d.ts +2 -0
- package/dist/lib/core-utils/worktree-resume.js +16 -0
- package/dist/lib/daemon/migrate.d.ts +8 -1
- package/dist/lib/daemon/migrate.js +19 -7
- package/dist/lib/doctor/checks/claude-wiring.js +9 -0
- package/dist/lib/lanes/autopilot-metrics.d.ts +99 -0
- package/dist/lib/lanes/autopilot-metrics.js +301 -0
- package/dist/lib/lanes/flag-keys.d.ts +2 -0
- package/dist/lib/lanes/flag-keys.js +2 -0
- package/dist/lib/lanes/manager-status-gate.d.ts +0 -17
- package/dist/lib/lanes/manager-status-gate.js +2 -43
- package/dist/lib/lanes/outpost-dispatch.d.ts +2 -0
- package/dist/lib/lanes/outpost-dispatch.js +7 -1
- package/dist/lib/lanes/outpost-hire-cache.d.ts +0 -1
- package/dist/lib/lanes/outpost-hire-cache.js +0 -1
- package/dist/lib/lanes/outpost-snapshot-memo.d.ts +6 -0
- package/dist/lib/lanes/outpost-snapshot-memo.js +14 -0
- package/dist/lib/lanes/reconcile.d.ts +2 -3
- package/dist/lib/lanes/reconcile.js +44 -38
- package/dist/lib/lanes/spawn.d.ts +0 -2
- package/dist/lib/lanes/spawn.js +18 -5
- package/dist/lib/lanes/store.d.ts +2 -0
- package/dist/lib/lanes/store.js +54 -3
- package/dist/lib/lanes/types.d.ts +18 -1
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/lanes/watcher.d.ts +4 -3
- package/dist/lib/lanes/watcher.js +102 -34
- package/dist/lib/mesh/live/daemon/run.js +18 -1
- package/dist/native-fast-route.d.ts +3 -1
- package/dist/native-fast-route.js +10 -8
- package/dist/utils/cognito-session.d.ts +6 -0
- package/dist/utils/cognito-session.js +9 -0
- package/dist/utils/contribution-table.d.ts +1 -1
- package/dist/utils/fast-stream-errors.d.ts +12 -0
- package/dist/utils/fast-stream-errors.js +18 -0
- package/dist/utils/scaffold-fast-import-guard.d.ts +2 -2
- package/dist/utils/scaffold-fast-import-guard.js +6 -3
- package/dist/utils/self-update.js +31 -0
- package/dist/utils/version-gate.js +22 -3
- package/package.json +1 -1
- package/dist/fast-lanes-list-route.d.ts +0 -8
- package/dist/fast-lanes-list-route.js +0 -32
- package/dist/fast-lanes-list.d.ts +0 -3
- package/dist/fast-lanes-list.js +0 -224
- package/dist/lib/doctor/client-health-sentry-flag.d.ts +0 -23
- package/dist/lib/doctor/client-health-sentry-flag.js +0 -99
- package/dist/lib/doctor/client-health-sentry.d.ts +0 -71
- package/dist/lib/doctor/client-health-sentry.js +0 -286
- package/dist/lib/lanes/outpost-snapshot-cache.d.ts +0 -26
- package/dist/lib/lanes/outpost-snapshot-cache.js +0 -215
- package/dist/lib/lanes/outpost-snapshot-refresh.d.ts +0 -21
- package/dist/lib/lanes/outpost-snapshot-refresh.js +0 -329
package/dist/commands/lanes.js
CHANGED
|
@@ -17,11 +17,13 @@ import { createLaneMoveHandlers } from "./lanes-move.js";
|
|
|
17
17
|
import { createLaneQuestionsHandlers } from "./lanes-questions.js";
|
|
18
18
|
import { createLaneResumeHandlers } from "./lanes-resume.js";
|
|
19
19
|
import { createLaneWatchHandlers } from "./lanes-watch.js";
|
|
20
|
+
import { createLaneStoryHandlers } from "./lanes-stories.js";
|
|
20
21
|
import { callerMatchesSessionSenior, assertPlacementEnabled, defaultLanePlacementEnabled, parsePlacementPolicy, processLanePlacementCompany, readClaudeAccountBalanceSignalsEnabled, readLanePlacementEnabled, } from "./lanes-shared.js";
|
|
21
22
|
export { CLAUDE_ACCOUNT_BALANCE_SIGNALS_FLAG, LANE_OUTPOST_LOAD_BALANCING_FLAG } from "./lanes-shared.js";
|
|
22
23
|
export { resolveAnswerChoice } from "./lanes-questions.js";
|
|
23
24
|
import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
|
|
24
25
|
import { registerLaneKpisCommand } from "./lanes-kpis.js";
|
|
26
|
+
import { registerLaneAutopilotReportCommand } from "./lanes-autopilot.js";
|
|
25
27
|
import { registerLaneChecksCommand } from "./lanes-checks.js";
|
|
26
28
|
import { registerLaneReportCommands } from "./lanes-report.js";
|
|
27
29
|
import { registerLaneRepoMergeHoldCommand } from "./lanes-hold.js";
|
|
@@ -29,18 +31,17 @@ import { registerLaneReviewCommand } from "./lanes-review.js";
|
|
|
29
31
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
30
32
|
import { withCliFeatureUsed } from "../utils/cli-telemetry.js";
|
|
31
33
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
32
|
-
import { startOutpostLaneSnapshotRefresh } from "../lib/lanes/outpost-snapshot-refresh.js";
|
|
33
34
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
34
35
|
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
35
36
|
import { runLeadAutopilotPass } from "../lib/lanes/autopilot.js";
|
|
36
|
-
import {
|
|
37
|
+
import { OutpostSnapshotInvocationMemo } from "../lib/lanes/outpost-snapshot-memo.js";
|
|
37
38
|
import { createOutpostHireGrantContext, parseOutpostHireGrantContext, selectOutpostHireGrantHierarchy, sameHireGrantSenior, } from "../lib/lanes/hire-grant-dispatch.js";
|
|
38
39
|
import { createOutpostProfileRepinContext, parseOutpostProfileRepinContext, } from "../lib/lanes/outpost-profile-repin.js";
|
|
39
40
|
import { createOutpostRegistryWorkerApprovalContext, parseOutpostRegistryWorkerApprovalContext, } from "../lib/lanes/outpost-registry-worker-approval.js";
|
|
40
41
|
import { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity, resolveAdmissionHost, } from "../lib/lanes/admission-host.js";
|
|
41
42
|
export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
42
43
|
import { buildOutpostCapacityCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand, buildOutpostQuestionListCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, buildOutpostLaneListCommand, REMOTE_LANE_READ_ENV, outpostRejectedEffortOption, parseOutpostPickupPayload, PICKUP_WAIT_MS, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
|
|
43
|
-
import { outpostHireSummaryCacheKey,
|
|
44
|
+
import { outpostHireSummaryCacheKey, OUTPOST_HIRE_SUMMARY_CACHE_TTL_MS, readOutpostHireSummaryCache, readOutpostHireSummaryCacheIncludingStale, writeOutpostHireSummaryCache, } from "../lib/lanes/outpost-hire-cache.js";
|
|
44
45
|
import { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "../lib/lanes/host-capacity.js";
|
|
45
46
|
import { BALANCE_MOVABLE_STATES, selectClaudeAccount, } from "../lib/lanes/claude-balance.js";
|
|
46
47
|
import { parseUserYaml } from "../utils/user-yaml-error.js";
|
|
@@ -55,7 +56,7 @@ import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
|
55
56
|
import { listLaneReviews } from "../lib/lanes/reviews.js";
|
|
56
57
|
import { effectiveAccountHome } from "../lib/lanes/account-home.js";
|
|
57
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 {
|
|
59
|
+
import { laneStories, storyWarning, } from "../lib/lanes/stories.js";
|
|
59
60
|
import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
|
|
60
61
|
import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
|
|
61
62
|
import { doneKeepAliveVerdict, hasPendingKeepAliveWork, keepAliveExclusion, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
|
|
@@ -67,7 +68,7 @@ import { LanesError } from "../lib/lanes/errors.js";
|
|
|
67
68
|
import { readLatestEnvelopeRecord } from "../lib/lanes/watcher.js";
|
|
68
69
|
import { loadQuotaState, quotaBlockFor } from "../lib/lanes/quota.js";
|
|
69
70
|
import { SESSION_MOVE_REASON } from "../lib/lanes/move-account.js";
|
|
70
|
-
import {
|
|
71
|
+
import { resolveManagerStatusTarget, } from "../lib/lanes/manager-status-gate.js";
|
|
71
72
|
import { blockerFreshnessFingerprint, resolveBlockerFreshnessForLane, } from "../lib/lanes/blocker-freshness.js";
|
|
72
73
|
import { resolveKrFreshnessForShow } from "../lib/lanes/kr-freshness.js";
|
|
73
74
|
import { ensureSessionHome } from "../lib/lanes/session-relink.js";
|
|
@@ -83,7 +84,6 @@ const REMOTE_MIRROR_REFRESH_BATCH_SIZE = 100;
|
|
|
83
84
|
const REMOTE_LANE_NOT_FOUND_GRACE_MS = 15 * 60_000;
|
|
84
85
|
/** The lane → mesh seam. Defined in lib/lanes/mesh-events.ts; the watcher shares it. */
|
|
85
86
|
export { emitLaneDecisionEvents };
|
|
86
|
-
export { LANE_OUTPOST_SNAPSHOT_CACHE_FLAG };
|
|
87
87
|
function withoutProviderHome(value) {
|
|
88
88
|
if (Array.isArray(value))
|
|
89
89
|
return value.map(withoutProviderHome);
|
|
@@ -299,8 +299,6 @@ function resolveDeps(deps = {}) {
|
|
|
299
299
|
})),
|
|
300
300
|
fetchText: deps.fetchText ?? defaultFetchText,
|
|
301
301
|
lanePlacementEnabled: deps.lanePlacementEnabled ?? defaultLanePlacementEnabled,
|
|
302
|
-
outpostSnapshotCacheEnabled: deps.outpostSnapshotCacheEnabled ?? (() => readLanesRegistryFlag(LANE_OUTPOST_SNAPSHOT_CACHE_FLAG, { env })),
|
|
303
|
-
startOutpostLaneSnapshotRefresh: deps.startOutpostLaneSnapshotRefresh ?? ((root) => startOutpostLaneSnapshotRefresh(root, env)),
|
|
304
302
|
storyLedgerEnabled: deps.storyLedgerEnabled ?? laneStoryLedgerEnabled,
|
|
305
303
|
isSsmOwnerDispatch: deps.isSsmOwnerDispatch ?? (() => isSsmRunCommandProcess()),
|
|
306
304
|
withGrantHireLocks: deps.withGrantHireLocks ?? withLaneHireLocks,
|
|
@@ -512,7 +510,7 @@ async function readRemoteHireGrantSenior(ctx, hqRoot, seniorLaneId) {
|
|
|
512
510
|
laneIds: [seniorLaneId],
|
|
513
511
|
snapshot: true,
|
|
514
512
|
strictSnapshot: true,
|
|
515
|
-
}, {}, {
|
|
513
|
+
}, {}, { bypassMemo: true });
|
|
516
514
|
}
|
|
517
515
|
catch (error) {
|
|
518
516
|
throw unavailable(isLanesError(error) ? error.error : "outpost_lane_list_unavailable");
|
|
@@ -1051,7 +1049,7 @@ function laneHostFromCtx(ctx, expectedId) {
|
|
|
1051
1049
|
return { kind: "local", id: hostname };
|
|
1052
1050
|
}
|
|
1053
1051
|
async function readOutpostCapacity(ctx, outpostId) {
|
|
1054
|
-
return
|
|
1052
|
+
return readOutpostCapacityUncached(ctx, outpostId);
|
|
1055
1053
|
}
|
|
1056
1054
|
async function readOutpostCapacityUncached(ctx, outpostId) {
|
|
1057
1055
|
let result;
|
|
@@ -1087,39 +1085,6 @@ async function readOutpostCapacityUncached(ctx, outpostId) {
|
|
|
1087
1085
|
}
|
|
1088
1086
|
return snapshot;
|
|
1089
1087
|
}
|
|
1090
|
-
async function readOutpostSnapshotWithCache(ctx, key, isValue, fetch) {
|
|
1091
|
-
let enabled = false;
|
|
1092
|
-
try {
|
|
1093
|
-
enabled = await ctx.outpostSnapshotCacheEnabled();
|
|
1094
|
-
}
|
|
1095
|
-
catch (err) {
|
|
1096
|
-
process.stderr.write(`lanes snapshot cache flag read failed; using uncached Outpost read: ${dispatchErrorMessage(err)}\n`);
|
|
1097
|
-
}
|
|
1098
|
-
if (!enabled)
|
|
1099
|
-
return fetch();
|
|
1100
|
-
return getOrFetchOutpostSnapshot({ hqRoot: ctx.hqRoot(), key, isValue, fetch });
|
|
1101
|
-
}
|
|
1102
|
-
function isLaneRecordArray(value) {
|
|
1103
|
-
if (!Array.isArray(value))
|
|
1104
|
-
return false;
|
|
1105
|
-
try {
|
|
1106
|
-
for (const lane of value)
|
|
1107
|
-
parseLaneRecord(lane);
|
|
1108
|
-
return true;
|
|
1109
|
-
}
|
|
1110
|
-
catch {
|
|
1111
|
-
return false;
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
function isOutpostCapacitySnapshot(value, outpostId) {
|
|
1115
|
-
try {
|
|
1116
|
-
const snapshot = parseHostCapacitySnapshot(value);
|
|
1117
|
-
return snapshot.host.kind === "outpost" && snapshot.host.id === outpostId;
|
|
1118
|
-
}
|
|
1119
|
-
catch {
|
|
1120
|
-
return false;
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
1088
|
async function loadHostCapacitySnapshots(ctx, hqRoot, config = loadLanesConfig(hqRoot), localLanes = loadAllLanes(hqRoot)) {
|
|
1124
1089
|
const localHost = laneHostFromCtx(ctx);
|
|
1125
1090
|
let outpostIds;
|
|
@@ -1278,7 +1243,7 @@ async function readOutpostLaneRecordsUncached(ctx, outpostId, query, dispatchOpt
|
|
|
1278
1243
|
}
|
|
1279
1244
|
async function readOutpostLaneRecords(ctx, outpostId, requestedQuery = {}, dispatchOptions = {}, options = {}) {
|
|
1280
1245
|
const query = normalizedLaneListQuery(requestedQuery);
|
|
1281
|
-
if (options.
|
|
1246
|
+
if (options.bypassMemo === true) {
|
|
1282
1247
|
return readOutpostLaneRecordsUncached(ctx, outpostId, query, dispatchOptions);
|
|
1283
1248
|
}
|
|
1284
1249
|
let invocationMemo = outpostLaneListMemo.get(ctx);
|
|
@@ -1286,10 +1251,10 @@ async function readOutpostLaneRecords(ctx, outpostId, requestedQuery = {}, dispa
|
|
|
1286
1251
|
invocationMemo = new OutpostSnapshotInvocationMemo();
|
|
1287
1252
|
outpostLaneListMemo.set(ctx, invocationMemo);
|
|
1288
1253
|
}
|
|
1289
|
-
return invocationMemo.getOrCreate(outpostId, query, () =>
|
|
1254
|
+
return invocationMemo.getOrCreate(outpostId, query, () => readOutpostLaneRecordsUncached(ctx, outpostId, query, dispatchOptions));
|
|
1290
1255
|
}
|
|
1291
|
-
/** Refresh
|
|
1292
|
-
async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds) {
|
|
1256
|
+
/** Refresh this host's selected Outpost mirrors in one bounded owner query per Outpost. */
|
|
1257
|
+
async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds, options = {}) {
|
|
1293
1258
|
const result = {
|
|
1294
1259
|
updated: new Map(),
|
|
1295
1260
|
unverifiedRemote: new Map(),
|
|
@@ -1314,11 +1279,21 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds) {
|
|
|
1314
1279
|
.filter((lane) => lane.host.kind === "outpost" &&
|
|
1315
1280
|
(host.outpostId === null || lane.host.id !== host.outpostId) &&
|
|
1316
1281
|
originMatchesThisHost(lane) &&
|
|
1317
|
-
(parentLaneIds === undefined || (lane.senior.kind === "lane" &&
|
|
1282
|
+
((parentLaneIds === undefined || (lane.senior.kind === "lane" &&
|
|
1318
1283
|
lane.senior.id !== undefined &&
|
|
1319
|
-
parentLaneIds.has(lane.senior.id))) &&
|
|
1320
|
-
!isTerminalLaneState(lane.state))
|
|
1321
|
-
.sort((a, b) =>
|
|
1284
|
+
parentLaneIds.has(lane.senior.id))) || options.laneIds?.has(lane.lane_id) === true) &&
|
|
1285
|
+
(options.includeTerminalMirrors === true || !isTerminalLaneState(lane.state)))
|
|
1286
|
+
.sort((a, b) => {
|
|
1287
|
+
const terminalOrder = Number(isTerminalLaneState(a.state)) - Number(isTerminalLaneState(b.state));
|
|
1288
|
+
if (terminalOrder !== 0)
|
|
1289
|
+
return terminalOrder;
|
|
1290
|
+
if (isTerminalLaneState(a.state)) {
|
|
1291
|
+
const updatedAtOrder = Date.parse(b.updated_at) - Date.parse(a.updated_at);
|
|
1292
|
+
if (updatedAtOrder !== 0)
|
|
1293
|
+
return updatedAtOrder;
|
|
1294
|
+
}
|
|
1295
|
+
return a.lane_id.localeCompare(b.lane_id);
|
|
1296
|
+
});
|
|
1322
1297
|
if (candidates.length === 0)
|
|
1323
1298
|
return result;
|
|
1324
1299
|
const enabledByCompany = new Map();
|
|
@@ -1404,7 +1379,13 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds) {
|
|
|
1404
1379
|
: undefined;
|
|
1405
1380
|
const before = readLane(hqRoot, mirror.lane_id);
|
|
1406
1381
|
if (before.host.kind !== "outpost" || before.host.id !== outpostId ||
|
|
1407
|
-
!originMatchesThisHost(before) ||
|
|
1382
|
+
!originMatchesThisHost(before) ||
|
|
1383
|
+
(!options.includeTerminalMirrors && isTerminalLaneState(before.state))) {
|
|
1384
|
+
result.updated.set(mirror.lane_id, before);
|
|
1385
|
+
continue;
|
|
1386
|
+
}
|
|
1387
|
+
if (owner.version < before.version) {
|
|
1388
|
+
result.unverifiedRemote.set(mirror.lane_id, "owner_snapshot_behind_mirror");
|
|
1408
1389
|
result.updated.set(mirror.lane_id, before);
|
|
1409
1390
|
continue;
|
|
1410
1391
|
}
|
|
@@ -1422,10 +1403,16 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds) {
|
|
|
1422
1403
|
result.updated.set(mirror.lane_id, before);
|
|
1423
1404
|
continue;
|
|
1424
1405
|
}
|
|
1406
|
+
let ownerSnapshotLostVersionRace = false;
|
|
1425
1407
|
const updated = retryUpdateLane(hqRoot, mirror.lane_id, "reconcile", (current) => {
|
|
1426
1408
|
if (current.host.kind !== "outpost" || current.host.id !== outpostId ||
|
|
1427
|
-
!originMatchesThisHost(current) ||
|
|
1409
|
+
!originMatchesThisHost(current) ||
|
|
1410
|
+
(!options.includeTerminalMirrors && isTerminalLaneState(current.state)))
|
|
1411
|
+
return current;
|
|
1412
|
+
if (owner.version < current.version) {
|
|
1413
|
+
ownerSnapshotLostVersionRace = true;
|
|
1428
1414
|
return current;
|
|
1415
|
+
}
|
|
1429
1416
|
return {
|
|
1430
1417
|
...current,
|
|
1431
1418
|
state: owner.state,
|
|
@@ -1443,6 +1430,9 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds) {
|
|
|
1443
1430
|
} : {}),
|
|
1444
1431
|
};
|
|
1445
1432
|
});
|
|
1433
|
+
if (ownerSnapshotLostVersionRace) {
|
|
1434
|
+
result.unverifiedRemote.set(mirror.lane_id, "owner_snapshot_behind_mirror");
|
|
1435
|
+
}
|
|
1446
1436
|
result.updated.set(mirror.lane_id, updated);
|
|
1447
1437
|
}
|
|
1448
1438
|
}
|
|
@@ -1522,6 +1512,7 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
|
|
|
1522
1512
|
const hostOutpostIdSet = new Set(hostOutpostIds);
|
|
1523
1513
|
const matches = [];
|
|
1524
1514
|
const failedOutpostIds = [];
|
|
1515
|
+
const probedOutpostIds = [];
|
|
1525
1516
|
const lookupOutpostIds = requestedOutpostId !== undefined
|
|
1526
1517
|
? [requestedOutpostId]
|
|
1527
1518
|
: [
|
|
@@ -1535,6 +1526,7 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
|
|
|
1535
1526
|
hierarchy: [laneId],
|
|
1536
1527
|
snapshot: true,
|
|
1537
1528
|
});
|
|
1529
|
+
probedOutpostIds.push(outpostId);
|
|
1538
1530
|
}
|
|
1539
1531
|
catch (err) {
|
|
1540
1532
|
if (requestedOutpostId !== undefined)
|
|
@@ -1553,7 +1545,10 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
|
|
|
1553
1545
|
}
|
|
1554
1546
|
}
|
|
1555
1547
|
if (matches.length === 0) {
|
|
1556
|
-
throw commandLaneNotFound(laneId, options.outpostHint === true,
|
|
1548
|
+
throw commandLaneNotFound(laneId, options.outpostHint === true, {
|
|
1549
|
+
...(failedOutpostIds.length > 0 ? { failed_outpost_ids: failedOutpostIds } : {}),
|
|
1550
|
+
...(probedOutpostIds.length > 0 ? { probed_outpost_ids: probedOutpostIds } : {}),
|
|
1551
|
+
});
|
|
1557
1552
|
}
|
|
1558
1553
|
if (matches.length !== 1) {
|
|
1559
1554
|
throw new LanesError("lane_outpost_ambiguous", `More than one registered Outpost reported lane ${laneId} with the same recorded host identity`, { lane_id: laneId, outpost_ids: matches.map((match) => match.outpostId) });
|
|
@@ -3500,7 +3495,6 @@ async function runCreate(ctx, opts) {
|
|
|
3500
3495
|
if (admission.action === "start") {
|
|
3501
3496
|
started = spawnLaneWorker(hqRoot, lane, {
|
|
3502
3497
|
env: ctx.env,
|
|
3503
|
-
managerStatusGateEnabled: await managerStatusGateEnabledForLane(hqRoot, lane, { env: ctx.env }),
|
|
3504
3498
|
modelDeps: {
|
|
3505
3499
|
readFileSync: ctx.readFileSync,
|
|
3506
3500
|
probe: ctx.probeProvider,
|
|
@@ -4274,6 +4268,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
|
|
|
4274
4268
|
outpost_id: outpostId,
|
|
4275
4269
|
outpost_command_id: dispatched.commandId,
|
|
4276
4270
|
outpost_transport: "outposts_exec",
|
|
4271
|
+
delivered_to: { host: outpostId, lane_id: ownerLane.lane_id, mechanism: "outposts_exec" },
|
|
4277
4272
|
});
|
|
4278
4273
|
return;
|
|
4279
4274
|
}
|
|
@@ -4304,6 +4299,9 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
|
|
|
4304
4299
|
outpost_id: outpostId,
|
|
4305
4300
|
outpost_command_id: dispatched.commandId,
|
|
4306
4301
|
outpost_transport: "outposts_exec",
|
|
4302
|
+
...(action === "message"
|
|
4303
|
+
? { delivered_to: { host: outpostId, lane_id: lane.lane_id, mechanism: "outposts_exec" } }
|
|
4304
|
+
: {}),
|
|
4307
4305
|
});
|
|
4308
4306
|
}
|
|
4309
4307
|
function dispatchedOwnerLane(ctx, laneId, outpostId, rawLane, behavior) {
|
|
@@ -4579,7 +4577,6 @@ async function runPickup(ctx, opts) {
|
|
|
4579
4577
|
try {
|
|
4580
4578
|
return spawnLaneWorker(hqRoot, current, {
|
|
4581
4579
|
env: ctx.env,
|
|
4582
|
-
managerStatusGateEnabled: await managerStatusGateEnabledForLane(hqRoot, current, { env: ctx.env }),
|
|
4583
4580
|
modelDeps: {
|
|
4584
4581
|
readFileSync: ctx.readFileSync,
|
|
4585
4582
|
probe: ctx.probeProvider,
|
|
@@ -4624,7 +4621,7 @@ async function runPickup(ctx, opts) {
|
|
|
4624
4621
|
lane: started,
|
|
4625
4622
|
});
|
|
4626
4623
|
}
|
|
4627
|
-
async function runList(ctx, opts
|
|
4624
|
+
async function runList(ctx, opts) {
|
|
4628
4625
|
if (opts.json && opts.human) {
|
|
4629
4626
|
throw new LanesError("conflicting_output_format", "Use either --json or --human for hq lanes list", { json: true, human: true });
|
|
4630
4627
|
}
|
|
@@ -4664,15 +4661,14 @@ async function runList(ctx, opts, fastRouteFlags) {
|
|
|
4664
4661
|
const hierarchyIds = opts.hierarchy === undefined
|
|
4665
4662
|
? undefined
|
|
4666
4663
|
: new Set(opts.hierarchy.flatMap((laneId) => selectOutpostHireGrantHierarchy(laneId, laneSource).map((lane) => lane.lane_id)));
|
|
4667
|
-
|
|
4668
|
-
.filter((lane) => {
|
|
4664
|
+
const matchesLaneFilters = (lane, includeState) => {
|
|
4669
4665
|
if (requestedLaneIds.length > 0 && !requestedIds.has(lane.lane_id))
|
|
4670
4666
|
return false;
|
|
4671
|
-
if (opts.active && isTerminalLaneState(lane.state))
|
|
4667
|
+
if (includeState && opts.active && isTerminalLaneState(lane.state))
|
|
4672
4668
|
return false;
|
|
4673
4669
|
if (hierarchyIds && !hierarchyIds.has(lane.lane_id))
|
|
4674
4670
|
return false;
|
|
4675
|
-
if (state && lane.state !== state)
|
|
4671
|
+
if (includeState && state && lane.state !== state)
|
|
4676
4672
|
return false;
|
|
4677
4673
|
const carriesFilter = laneStories(lane).some((story) => (!opts.project || story.project_id === opts.project) &&
|
|
4678
4674
|
(!opts.story || story.story_id === opts.story));
|
|
@@ -4687,8 +4683,11 @@ async function runList(ctx, opts, fastRouteFlags) {
|
|
|
4687
4683
|
if (senior && seniorKey(lane.senior) !== seniorKey(senior))
|
|
4688
4684
|
return false;
|
|
4689
4685
|
return true;
|
|
4690
|
-
}
|
|
4686
|
+
};
|
|
4687
|
+
const selectLanes = () => laneSource
|
|
4688
|
+
.filter((lane) => matchesLaneFilters(lane, true))
|
|
4691
4689
|
.sort(compareLaneQueueOrder);
|
|
4690
|
+
let selectedLanes = selectLanes();
|
|
4692
4691
|
if (opts.snapshot) {
|
|
4693
4692
|
printJson({
|
|
4694
4693
|
ok: true,
|
|
@@ -4705,15 +4704,20 @@ async function runList(ctx, opts, fastRouteFlags) {
|
|
|
4705
4704
|
const remoteRead = ctx.env[REMOTE_LANE_READ_ENV] === "1";
|
|
4706
4705
|
const remoteRefresh = remoteRead
|
|
4707
4706
|
? { updated: new Map(), unverifiedRemote: new Map(), missingOnOwner: new Map() }
|
|
4708
|
-
: await refreshRemoteLaneMirrors(ctx, hqRoot, laneSource, new Set(
|
|
4709
|
-
.filter((lane) =>
|
|
4710
|
-
.map((lane) => lane.lane_id))
|
|
4707
|
+
: await refreshRemoteLaneMirrors(ctx, hqRoot, laneSource, new Set(laneSource
|
|
4708
|
+
.filter((lane) => matchesLaneFilters(lane, false))
|
|
4709
|
+
.map((lane) => lane.lane_id)), {
|
|
4710
|
+
includeTerminalMirrors: true,
|
|
4711
|
+
laneIds: new Set(laneSource
|
|
4712
|
+
.filter((lane) => matchesLaneFilters(lane, false))
|
|
4713
|
+
.map((lane) => lane.lane_id)),
|
|
4714
|
+
});
|
|
4711
4715
|
for (const [laneId, updated] of remoteRefresh.updated) {
|
|
4712
4716
|
const index = laneSource.findIndex((lane) => lane.lane_id === laneId);
|
|
4713
4717
|
if (index >= 0)
|
|
4714
4718
|
laneSource[index] = updated;
|
|
4715
4719
|
}
|
|
4716
|
-
selectedLanes =
|
|
4720
|
+
selectedLanes = selectLanes();
|
|
4717
4721
|
const now = ctx.now();
|
|
4718
4722
|
const host = reconcileHostFromCtx(ctx);
|
|
4719
4723
|
const allLanes = laneSource;
|
|
@@ -4734,33 +4738,7 @@ async function runList(ctx, opts, fastRouteFlags) {
|
|
|
4734
4738
|
ownerEligibleLaneIds.add(lane.lane_id);
|
|
4735
4739
|
}
|
|
4736
4740
|
const ownerLanes = selectedLanes.filter((lane) => ownerEligibleLaneIds.has(lane.lane_id));
|
|
4737
|
-
const refreshRemote = ctx.env.HQ_INTERNAL_LANES_REFRESH_REMOTE_SNAPSHOT === "1";
|
|
4738
|
-
let snapshotCacheEnabled = fastRouteFlags?.snapshotCacheEnabled ?? false;
|
|
4739
|
-
if (fastRouteFlags === undefined &&
|
|
4740
|
-
(refreshRemote || (!opts.fresh && ownerLanes.length > 0))) {
|
|
4741
|
-
try {
|
|
4742
|
-
snapshotCacheEnabled = await ctx.outpostSnapshotCacheEnabled();
|
|
4743
|
-
}
|
|
4744
|
-
catch (err) {
|
|
4745
|
-
process.stderr.write(`lanes snapshot cache flag read failed; using wait-for-remote list: ${dispatchErrorMessage(err)}\n`);
|
|
4746
|
-
}
|
|
4747
|
-
}
|
|
4748
|
-
if (refreshRemote) {
|
|
4749
|
-
if (snapshotCacheEnabled && ownerLanes.length > 0) {
|
|
4750
|
-
const outcomes = await batchOwnerLaneHireFields(ctx, ownerLanes, allLanes, { forceRefresh: true });
|
|
4751
|
-
const failures = [...outcomes.entries()].filter(([, outcome]) => "error" in outcome);
|
|
4752
|
-
if (failures.length > 0) {
|
|
4753
|
-
const firstOutcome = failures[0][1];
|
|
4754
|
-
const firstError = "error" in firstOutcome ? firstOutcome.error : undefined;
|
|
4755
|
-
process.stderr.write(`lanes snapshot refresh failed for ${failures.length} lane(s): ${firstError?.error ?? "outpost_lane_hire_summary_missing"}: ${firstError?.message ?? "owner summary outcome was missing"}\n`);
|
|
4756
|
-
throw firstError ?? new LanesError("outpost_lane_hire_summary_missing", "Background Outpost lane summary refresh returned a missing outcome", { lane_ids: failures.map(([laneId]) => laneId) });
|
|
4757
|
-
}
|
|
4758
|
-
}
|
|
4759
|
-
return;
|
|
4760
|
-
}
|
|
4761
4741
|
const hireByLane = new Map();
|
|
4762
|
-
const remoteSnapshotMetadata = new Map();
|
|
4763
|
-
let remoteLanesLoading = false;
|
|
4764
4742
|
for (const lane of selectedLanes) {
|
|
4765
4743
|
if (!ownerEligibleLaneIds.has(lane.lane_id)) {
|
|
4766
4744
|
hireByLane.set(lane.lane_id, {
|
|
@@ -4770,47 +4748,7 @@ async function runList(ctx, opts, fastRouteFlags) {
|
|
|
4770
4748
|
continue;
|
|
4771
4749
|
}
|
|
4772
4750
|
}
|
|
4773
|
-
if (
|
|
4774
|
-
const nowMs = ctx.now().getTime();
|
|
4775
|
-
const cached = readOutpostHireSummaryCacheIncludingStale(hqRoot, nowMs);
|
|
4776
|
-
let refreshNeeded = false;
|
|
4777
|
-
for (const lane of ownerLanes) {
|
|
4778
|
-
if (lane.host.kind !== "outpost")
|
|
4779
|
-
continue;
|
|
4780
|
-
const summary = cached.get(outpostHireSummaryCacheKey(lane.host.id, lane.lane_id));
|
|
4781
|
-
if (!summary) {
|
|
4782
|
-
hireByLane.set(lane.lane_id, {
|
|
4783
|
-
...laneHireFields(lane, allLanes, aliveChildrenByParent),
|
|
4784
|
-
hire_source: "local",
|
|
4785
|
-
});
|
|
4786
|
-
remoteSnapshotMetadata.set(lane.lane_id, { ageSeconds: null, stale: true, loading: true });
|
|
4787
|
-
refreshNeeded = true;
|
|
4788
|
-
remoteLanesLoading = true;
|
|
4789
|
-
continue;
|
|
4790
|
-
}
|
|
4791
|
-
const ageMs = Math.max(0, nowMs - Date.parse(summary.cached_at));
|
|
4792
|
-
const stale = ageMs >= OUTPOST_SNAPSHOT_CACHE_TTL_MS;
|
|
4793
|
-
hireByLane.set(lane.lane_id, {
|
|
4794
|
-
hire_grant: summary.hire_grant,
|
|
4795
|
-
hire_alive_children: summary.hire_alive_children,
|
|
4796
|
-
hire: summary.hire,
|
|
4797
|
-
hire_summary: summary.hire_summary,
|
|
4798
|
-
hire_source: "owner",
|
|
4799
|
-
});
|
|
4800
|
-
remoteSnapshotMetadata.set(lane.lane_id, {
|
|
4801
|
-
ageSeconds: Math.floor(ageMs / 1_000),
|
|
4802
|
-
stale,
|
|
4803
|
-
loading: stale,
|
|
4804
|
-
});
|
|
4805
|
-
if (stale) {
|
|
4806
|
-
refreshNeeded = true;
|
|
4807
|
-
remoteLanesLoading = true;
|
|
4808
|
-
}
|
|
4809
|
-
}
|
|
4810
|
-
if (refreshNeeded)
|
|
4811
|
-
ctx.startOutpostLaneSnapshotRefresh(hqRoot);
|
|
4812
|
-
}
|
|
4813
|
-
else if (ownerLanes.length > 0) {
|
|
4751
|
+
if (ownerLanes.length > 0) {
|
|
4814
4752
|
const ownerSummaries = await batchOwnerLaneHireFields(ctx, ownerLanes, allLanes);
|
|
4815
4753
|
for (const lane of ownerLanes) {
|
|
4816
4754
|
const outcome = ownerSummaries.get(lane.lane_id);
|
|
@@ -4844,15 +4782,6 @@ async function runList(ctx, opts, fastRouteFlags) {
|
|
|
4844
4782
|
...(remoteQueued
|
|
4845
4783
|
? { state: "remote", remote_state: lane.state, queued_reason: undefined, queued_details: undefined }
|
|
4846
4784
|
: {}),
|
|
4847
|
-
...(remoteSnapshotMetadata.has(lane.lane_id)
|
|
4848
|
-
? {
|
|
4849
|
-
remote_snapshot_age_s: remoteSnapshotMetadata.get(lane.lane_id).ageSeconds,
|
|
4850
|
-
remote_snapshot_stale: remoteSnapshotMetadata.get(lane.lane_id).stale,
|
|
4851
|
-
...(remoteSnapshotMetadata.get(lane.lane_id).loading
|
|
4852
|
-
? { remote_snapshot_loading: true }
|
|
4853
|
-
: {}),
|
|
4854
|
-
}
|
|
4855
|
-
: {}),
|
|
4856
4785
|
overdue: Boolean(lane.overdue) || isOverdue(hqRoot, lane.lane_id, now),
|
|
4857
4786
|
stalled: Boolean(lane.stalled),
|
|
4858
4787
|
// Listing is observational. Reconcile owns durable liveness transitions.
|
|
@@ -4876,8 +4805,7 @@ async function runList(ctx, opts, fastRouteFlags) {
|
|
|
4876
4805
|
};
|
|
4877
4806
|
});
|
|
4878
4807
|
const placementCompany = company ?? processLanePlacementCompany(ctx);
|
|
4879
|
-
const placementEnabled = !remoteRead && (
|
|
4880
|
-
await readLanePlacementEnabled(ctx, placementCompany));
|
|
4808
|
+
const placementEnabled = !remoteRead && await readLanePlacementEnabled(ctx, placementCompany);
|
|
4881
4809
|
const host_capacity = placementEnabled
|
|
4882
4810
|
? (await loadHostCapacitySnapshots(ctx, hqRoot, undefined, allLanes)).entries
|
|
4883
4811
|
: undefined;
|
|
@@ -4888,43 +4816,24 @@ async function runList(ctx, opts, fastRouteFlags) {
|
|
|
4888
4816
|
...(remoteRefresh.unverifiedRemote.size > 0
|
|
4889
4817
|
? { unverified_remote_lanes: [...remoteRefresh.unverifiedRemote.keys()] }
|
|
4890
4818
|
: {}),
|
|
4891
|
-
...(remoteLanesLoading ? { remote_lanes_loading: true } : {}),
|
|
4892
4819
|
...(host_capacity !== undefined ? { host_capacity } : {}),
|
|
4893
4820
|
};
|
|
4894
4821
|
if (opts.human)
|
|
4895
|
-
console.log(formatLaneListHuman(lanes
|
|
4822
|
+
console.log(formatLaneListHuman(lanes));
|
|
4896
4823
|
else
|
|
4897
4824
|
printJson(result);
|
|
4898
4825
|
}
|
|
4899
|
-
|
|
4900
|
-
export async function runFastCachedLanesListFromEntrypoint() {
|
|
4901
|
-
try {
|
|
4902
|
-
await runList(resolveDeps(), { json: true }, {
|
|
4903
|
-
snapshotCacheEnabled: true,
|
|
4904
|
-
lanePlacementEnabled: true,
|
|
4905
|
-
});
|
|
4906
|
-
}
|
|
4907
|
-
catch (err) {
|
|
4908
|
-
failJson(err);
|
|
4909
|
-
}
|
|
4910
|
-
}
|
|
4911
|
-
function formatLaneListHuman(lanes, remoteLanesLoading = false) {
|
|
4826
|
+
function formatLaneListHuman(lanes) {
|
|
4912
4827
|
const rows = lanes.map((lane) => {
|
|
4913
4828
|
const ids = lane.story_ids;
|
|
4914
4829
|
const storyLabel = ids.length > 0
|
|
4915
4830
|
? "stories: " + ids.join(", ")
|
|
4916
4831
|
: "no story: " + (lane.no_story_reason ?? "reason not recorded");
|
|
4917
|
-
const snapshotNote = lane.remote_snapshot_stale && lane.remote_snapshot_age_s !== null &&
|
|
4918
|
-
lane.remote_snapshot_age_s !== undefined
|
|
4919
|
-
? ` remote snapshot stale (${lane.remote_snapshot_age_s}s)`
|
|
4920
|
-
: "";
|
|
4921
4832
|
return lane.lane_id + " " + lane.state + " " + lane.provider + " " + lane.project_id +
|
|
4922
|
-
" " + storyLabel
|
|
4833
|
+
" " + storyLabel;
|
|
4923
4834
|
});
|
|
4924
4835
|
if (rows.length === 0)
|
|
4925
4836
|
rows.push("No lanes.");
|
|
4926
|
-
if (remoteLanesLoading)
|
|
4927
|
-
rows.push("Remote lanes loading.");
|
|
4928
4837
|
return rows.join("\n");
|
|
4929
4838
|
}
|
|
4930
4839
|
async function runStatus(ctx, opts) {
|
|
@@ -5119,8 +5028,17 @@ async function runShow(ctx, laneId) {
|
|
|
5119
5028
|
const doneStateVerdict = storedLane.state === "done" && storedLane.keep_alive === true
|
|
5120
5029
|
? doneKeepAliveVerdict(storedLane, { decision: storedLane.last_envelope?.decision, story_complete: storedLane.story_complete }, storedLane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, storedLane))
|
|
5121
5030
|
: null;
|
|
5122
|
-
|
|
5123
|
-
|
|
5031
|
+
let managerStatusTarget;
|
|
5032
|
+
let managerStatusGateError;
|
|
5033
|
+
try {
|
|
5034
|
+
resolveLaneCompany(hqRoot, storedLane);
|
|
5035
|
+
managerStatusTarget = resolveManagerStatusTarget(hqRoot, storedLane);
|
|
5036
|
+
}
|
|
5037
|
+
catch (err) {
|
|
5038
|
+
managerStatusGateError = err instanceof LanesError ? err.error : "manager_status_gate_unavailable";
|
|
5039
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5040
|
+
process.stderr.write(`hq lanes: manager status gate disabled for ${storedLane.lane_id}: ${managerStatusGateError}: ${message}\n`);
|
|
5041
|
+
}
|
|
5124
5042
|
const blockerFreshness = await resolveBlockerFreshnessForLane(hqRoot, storedLane, {
|
|
5125
5043
|
env: ctx.env,
|
|
5126
5044
|
now: ctx.now().getTime(),
|
|
@@ -5161,6 +5079,7 @@ async function runShow(ctx, laneId) {
|
|
|
5161
5079
|
envelope_repair_mode: lane.envelope_repair?.mode ?? null,
|
|
5162
5080
|
envelope_validation_errors: lane.envelope_validation_errors ?? [],
|
|
5163
5081
|
last_resume: lane.last_resume ?? null,
|
|
5082
|
+
keep_alive_history: lane.keep_alive_history ?? [],
|
|
5164
5083
|
account: lane.account ?? null,
|
|
5165
5084
|
account_history: lane.account_history ?? [],
|
|
5166
5085
|
failed_resume_history: lane.failed_resume_history ?? [],
|
|
@@ -5184,15 +5103,16 @@ async function runShow(ctx, laneId) {
|
|
|
5184
5103
|
(storedLane.keep_alive === true ? null : "not_opted_in"),
|
|
5185
5104
|
consecutive_auto_resumes: lane.keep_alive_resume_count ?? 0,
|
|
5186
5105
|
last_auto_resume_at: lane.keep_alive_last_resume_at ?? null,
|
|
5106
|
+
keep_alive_history: lane.keep_alive_history ?? [],
|
|
5187
5107
|
next_eligible_at: lane.keep_alive_next_eligible_at ?? null,
|
|
5188
5108
|
exhausted_at: lane.keep_alive_exhausted_at ?? null,
|
|
5189
5109
|
},
|
|
5190
|
-
manager_status_gate:
|
|
5110
|
+
manager_status_gate: managerStatusGateError !== undefined
|
|
5191
5111
|
? {
|
|
5192
5112
|
status_path: null,
|
|
5193
5113
|
skipped: null,
|
|
5194
5114
|
consecutive_misses: storedLane.status_gate?.consecutive_misses ?? 0,
|
|
5195
|
-
error:
|
|
5115
|
+
error: managerStatusGateError,
|
|
5196
5116
|
}
|
|
5197
5117
|
: managerStatusTarget?.manager
|
|
5198
5118
|
? {
|
|
@@ -5295,6 +5215,13 @@ function appendCallerSeniorReason(message, authorization) {
|
|
|
5295
5215
|
? message
|
|
5296
5216
|
: `${message}; authorization refused: ${callerSeniorReason(authorization)}`;
|
|
5297
5217
|
}
|
|
5218
|
+
function assertQuestionAnswerAuthorized(ctx, lane) {
|
|
5219
|
+
const authorization = callerSeniorAuthorization(ctx, lane.senior);
|
|
5220
|
+
if (authorization.authorized)
|
|
5221
|
+
return;
|
|
5222
|
+
const reason = callerSeniorReason(authorization);
|
|
5223
|
+
throw new LanesError("question_answer_unauthorized", appendCallerSeniorReason(`Only senior ${seniorKey(lane.senior)} may answer lane ${lane.lane_id}'s Outpost question`, authorization), { lane_id: lane.lane_id, senior: lane.senior, reason });
|
|
5224
|
+
}
|
|
5298
5225
|
function laneGrantTargetHostedHere(ctx, lane) {
|
|
5299
5226
|
if (lane.host.kind === "local") {
|
|
5300
5227
|
return laneRunsHere(lane, reconcileHostFromCtx(ctx));
|
|
@@ -5901,7 +5828,6 @@ async function runAdmit(ctx, laneId, opts) {
|
|
|
5901
5828
|
}
|
|
5902
5829
|
const started = spawnLaneWorker(hqRoot, lane, {
|
|
5903
5830
|
env: ctx.env,
|
|
5904
|
-
managerStatusGateEnabled: await managerStatusGateEnabledForLane(hqRoot, lane, { env: ctx.env }),
|
|
5905
5831
|
modelDeps: {
|
|
5906
5832
|
readFileSync: ctx.readFileSync,
|
|
5907
5833
|
probe: ctx.probeProvider,
|
|
@@ -6447,84 +6373,6 @@ function laneWithRecoveredProviderSession(hqRoot, lane) {
|
|
|
6447
6373
|
: {}),
|
|
6448
6374
|
};
|
|
6449
6375
|
}
|
|
6450
|
-
function storyCommandTarget(lane, storyId, project) {
|
|
6451
|
-
const projectId = project?.trim() || lane.project_id;
|
|
6452
|
-
const normalizedStoryId = storyId.trim();
|
|
6453
|
-
if (!projectId) {
|
|
6454
|
-
throw new LanesError("missing_project", "--project is required", {});
|
|
6455
|
-
}
|
|
6456
|
-
if (!normalizedStoryId) {
|
|
6457
|
-
throw new LanesError("missing_story", "<story-id> is required", {});
|
|
6458
|
-
}
|
|
6459
|
-
const story = laneStory(lane, projectId, normalizedStoryId);
|
|
6460
|
-
if (!story) {
|
|
6461
|
-
throw new LanesError("story_not_on_lane", `Story ${projectId}/${normalizedStoryId} is not carried by lane ${lane.lane_id}`, { lane_id: lane.lane_id, project_id: projectId, story_id: normalizedStoryId });
|
|
6462
|
-
}
|
|
6463
|
-
return { projectId, storyId: normalizedStoryId, story };
|
|
6464
|
-
}
|
|
6465
|
-
function updateCarriedStory(lane, projectId, storyId, status, at, extras = {}) {
|
|
6466
|
-
const stories = laneStories(lane).map((story) => story.project_id === projectId && story.story_id === storyId
|
|
6467
|
-
? {
|
|
6468
|
-
...story,
|
|
6469
|
-
status,
|
|
6470
|
-
updated_at: at,
|
|
6471
|
-
blocked_reason: extras.blocked_reason,
|
|
6472
|
-
verified_at: extras.verified_at,
|
|
6473
|
-
verified_by: extras.verified_by,
|
|
6474
|
-
}
|
|
6475
|
-
: story);
|
|
6476
|
-
return {
|
|
6477
|
-
...lane,
|
|
6478
|
-
project_id: projectId,
|
|
6479
|
-
story_id: storyId,
|
|
6480
|
-
stories,
|
|
6481
|
-
};
|
|
6482
|
-
}
|
|
6483
|
-
function addCarriedStory(lane, projectId, storyId, at) {
|
|
6484
|
-
if (laneStory(lane, projectId, storyId)) {
|
|
6485
|
-
throw new LanesError("story_already_on_lane", `Story ${projectId}/${storyId} is already carried by lane ${lane.lane_id}`, {
|
|
6486
|
-
lane_id: lane.lane_id,
|
|
6487
|
-
project_id: projectId,
|
|
6488
|
-
story_id: storyId,
|
|
6489
|
-
});
|
|
6490
|
-
}
|
|
6491
|
-
return {
|
|
6492
|
-
...lane,
|
|
6493
|
-
...(lane.story_id.trim() ? {} : { project_id: projectId, story_id: storyId }),
|
|
6494
|
-
stories: [
|
|
6495
|
-
...laneStories(lane),
|
|
6496
|
-
{ project_id: projectId, story_id: storyId, status: "in_progress", added_at: at, updated_at: at },
|
|
6497
|
-
],
|
|
6498
|
-
};
|
|
6499
|
-
}
|
|
6500
|
-
function callerMayVerifyStory(ctx, lane) {
|
|
6501
|
-
const callerLaneId = ctx.env.HQ_LANE_ID?.trim();
|
|
6502
|
-
if (callerLaneId) {
|
|
6503
|
-
return lane.senior.kind === "lane" && lane.senior.id === callerLaneId;
|
|
6504
|
-
}
|
|
6505
|
-
if (lane.senior.kind === "user")
|
|
6506
|
-
return true;
|
|
6507
|
-
if (lane.senior.kind === "lane")
|
|
6508
|
-
return false;
|
|
6509
|
-
return sessionIdFromEnv(ctx.env) === lane.senior.id;
|
|
6510
|
-
}
|
|
6511
|
-
async function ensureStoryThread(ctx, lane, projectId, storyId = lane.story_id) {
|
|
6512
|
-
const company = lane.mesh_company ?? lane.company;
|
|
6513
|
-
if (!company)
|
|
6514
|
-
return lane;
|
|
6515
|
-
return ensureLaneThreadRecord(ctx, lane, projectId, storyId);
|
|
6516
|
-
}
|
|
6517
|
-
async function ensureStoryThreadForTransition(ctx, lane, projectId, storyId) {
|
|
6518
|
-
try {
|
|
6519
|
-
return { lane: await ensureStoryThread(ctx, lane, projectId, storyId) };
|
|
6520
|
-
}
|
|
6521
|
-
catch (err) {
|
|
6522
|
-
const warning = storyNotOnBoardWarning(err);
|
|
6523
|
-
if (warning === undefined)
|
|
6524
|
-
throw err;
|
|
6525
|
-
return { lane, warning };
|
|
6526
|
-
}
|
|
6527
|
-
}
|
|
6528
6376
|
async function ensureLaneThreadRecord(ctx, lane, projectId = lane.project_id, storyId = lane.story_id, laneStatus = lane.state) {
|
|
6529
6377
|
const company = lane.mesh_company ?? lane.company;
|
|
6530
6378
|
if (!company)
|
|
@@ -6546,228 +6394,6 @@ async function ensureLaneThreadRecord(ctx, lane, projectId = lane.project_id, st
|
|
|
6546
6394
|
}
|
|
6547
6395
|
return updated;
|
|
6548
6396
|
}
|
|
6549
|
-
async function emitStoryTransition(ctx, lane, projectId, storyId, event, status, payload = {}, skipStoryStatusPatch = false) {
|
|
6550
|
-
const company = lane.mesh_company ?? lane.company;
|
|
6551
|
-
if (!company)
|
|
6552
|
-
return;
|
|
6553
|
-
const ready = await ensureStoryThread(ctx, lane, projectId, storyId);
|
|
6554
|
-
if (!ready.thread_id)
|
|
6555
|
-
return;
|
|
6556
|
-
await emitLaneMeshEvent(ctx, company, ready.thread_id, event, {
|
|
6557
|
-
lane_id: ready.lane_id,
|
|
6558
|
-
project_id: projectId,
|
|
6559
|
-
story_id: storyId,
|
|
6560
|
-
...payload,
|
|
6561
|
-
lane_status: lane.state,
|
|
6562
|
-
});
|
|
6563
|
-
if (!skipStoryStatusPatch && projectId?.trim() && storyId?.trim()) {
|
|
6564
|
-
await ctx.mesh.patchStoryStatus(company, projectId, storyId, status === "blocked" ? "in_progress" : status);
|
|
6565
|
-
}
|
|
6566
|
-
}
|
|
6567
|
-
function sameCommittedStory(current, expected, projectId, storyId) {
|
|
6568
|
-
const currentStory = laneStory(current, projectId, storyId);
|
|
6569
|
-
const expectedStory = laneStory(expected, projectId, storyId);
|
|
6570
|
-
return (current.project_id === expected.project_id &&
|
|
6571
|
-
current.story_id === expected.story_id &&
|
|
6572
|
-
JSON.stringify(currentStory) === JSON.stringify(expectedStory));
|
|
6573
|
-
}
|
|
6574
|
-
async function commitStoryTransition(ctx, before, next, projectId, storyId, event, status, payload = {}, skipStoryStatusPatch = false) {
|
|
6575
|
-
try {
|
|
6576
|
-
await emitStoryTransition(ctx, next, projectId, storyId, event, status, payload, skipStoryStatusPatch);
|
|
6577
|
-
return undefined;
|
|
6578
|
-
}
|
|
6579
|
-
catch (err) {
|
|
6580
|
-
const warning = storyNotOnBoardWarning(err);
|
|
6581
|
-
if (warning !== undefined)
|
|
6582
|
-
return warning;
|
|
6583
|
-
try {
|
|
6584
|
-
const current = readLane(ctx.hqRoot(), next.lane_id);
|
|
6585
|
-
if (!sameCommittedStory(current, next, projectId, storyId)) {
|
|
6586
|
-
throw new LanesError("story_transition_rollback_conflict", `Story ${projectId}/${storyId} changed while mesh publication failed`, { lane_id: next.lane_id, project_id: projectId, story_id: storyId });
|
|
6587
|
-
}
|
|
6588
|
-
updateLane(ctx.hqRoot(), next.lane_id, current.version, "command", (latest) => {
|
|
6589
|
-
if (!sameCommittedStory(latest, next, projectId, storyId)) {
|
|
6590
|
-
throw new LanesError("story_transition_rollback_conflict", `Story ${projectId}/${storyId} changed before rollback`, { lane_id: next.lane_id, project_id: projectId, story_id: storyId });
|
|
6591
|
-
}
|
|
6592
|
-
return {
|
|
6593
|
-
...latest,
|
|
6594
|
-
project_id: before.project_id,
|
|
6595
|
-
story_id: before.story_id,
|
|
6596
|
-
stories: before.stories,
|
|
6597
|
-
};
|
|
6598
|
-
});
|
|
6599
|
-
}
|
|
6600
|
-
catch (rollbackErr) {
|
|
6601
|
-
process.stderr.write(`hq lanes: story transition rollback failed for ${next.lane_id}/${projectId}/${storyId}: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}\n`);
|
|
6602
|
-
}
|
|
6603
|
-
throw err;
|
|
6604
|
-
}
|
|
6605
|
-
}
|
|
6606
|
-
async function runStoryList(ctx, laneId) {
|
|
6607
|
-
const lane = readLane(ctx.hqRoot(), laneId);
|
|
6608
|
-
printJson({
|
|
6609
|
-
ok: true,
|
|
6610
|
-
action: "story.list",
|
|
6611
|
-
lane_id: laneId,
|
|
6612
|
-
stories: laneStories(lane),
|
|
6613
|
-
active_stories: activeLaneStories(lane),
|
|
6614
|
-
story_warning: storyWarning(lane) ?? null,
|
|
6615
|
-
});
|
|
6616
|
-
}
|
|
6617
|
-
async function runStoryAdd(ctx, laneId, storyId, opts) {
|
|
6618
|
-
const hqRoot = ctx.hqRoot();
|
|
6619
|
-
let lane = readLane(hqRoot, laneId);
|
|
6620
|
-
assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
|
|
6621
|
-
const projectId = opts.project?.trim() || lane.project_id;
|
|
6622
|
-
if (!projectId)
|
|
6623
|
-
throw new LanesError("missing_project", "--project is required", {});
|
|
6624
|
-
const normalizedStoryId = storyId.trim();
|
|
6625
|
-
if (!normalizedStoryId)
|
|
6626
|
-
throw new LanesError("missing_story", "<story-id> is required", {});
|
|
6627
|
-
if (ctx.storyLedgerEnabled()) {
|
|
6628
|
-
validateProjectStories(hqRoot, projectId, [normalizedStoryId], lane.company?.slug);
|
|
6629
|
-
}
|
|
6630
|
-
const company = lane.mesh_company ?? lane.company;
|
|
6631
|
-
const warnings = [];
|
|
6632
|
-
let storyKnownAbsent = false;
|
|
6633
|
-
if (company) {
|
|
6634
|
-
const boardCheck = await storyBoardCheck(ctx, company, projectId, [normalizedStoryId]);
|
|
6635
|
-
if (boardCheck.warning !== undefined)
|
|
6636
|
-
warnings.push(boardCheck.warning);
|
|
6637
|
-
storyKnownAbsent = boardCheck.absentStoryIds.has(normalizedStoryId);
|
|
6638
|
-
}
|
|
6639
|
-
const at = ctx.now().toISOString();
|
|
6640
|
-
const thread = await ensureStoryThreadForTransition(ctx, lane, projectId, normalizedStoryId);
|
|
6641
|
-
lane = thread.lane;
|
|
6642
|
-
if (thread.warning !== undefined)
|
|
6643
|
-
warnings.push(thread.warning);
|
|
6644
|
-
const before = lane;
|
|
6645
|
-
const next = casUpdateLane(hqRoot, laneId, (current) => {
|
|
6646
|
-
assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
|
|
6647
|
-
const added = addCarriedStory(current, projectId, normalizedStoryId, at);
|
|
6648
|
-
return {
|
|
6649
|
-
...added,
|
|
6650
|
-
no_story_reason: undefined,
|
|
6651
|
-
};
|
|
6652
|
-
});
|
|
6653
|
-
const transitionWarning = await commitStoryTransition(ctx, before, next, projectId, normalizedStoryId, "lane_started", "in_progress", {}, storyKnownAbsent);
|
|
6654
|
-
if (transitionWarning !== undefined)
|
|
6655
|
-
warnings.push(transitionWarning);
|
|
6656
|
-
const finalLane = readLane(hqRoot, laneId);
|
|
6657
|
-
printJson({
|
|
6658
|
-
ok: true,
|
|
6659
|
-
action: "story.add",
|
|
6660
|
-
lane_id: laneId,
|
|
6661
|
-
project_id: projectId,
|
|
6662
|
-
story_id: normalizedStoryId,
|
|
6663
|
-
status: "in_progress",
|
|
6664
|
-
story_warning: combinedStoryWarning(finalLane, warnings),
|
|
6665
|
-
lane: finalLane,
|
|
6666
|
-
});
|
|
6667
|
-
}
|
|
6668
|
-
async function runStoryStart(ctx, laneId, storyId, opts) {
|
|
6669
|
-
const hqRoot = ctx.hqRoot();
|
|
6670
|
-
let lane = readLane(hqRoot, laneId);
|
|
6671
|
-
assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
|
|
6672
|
-
const target = storyCommandTarget(lane, storyId, opts.project);
|
|
6673
|
-
if (target.story.status === "done") {
|
|
6674
|
-
throw new LanesError("story_already_done", `Story ${target.projectId}/${target.storyId} is done and cannot be started again`, {
|
|
6675
|
-
lane_id: laneId,
|
|
6676
|
-
project_id: target.projectId,
|
|
6677
|
-
story_id: target.storyId,
|
|
6678
|
-
});
|
|
6679
|
-
}
|
|
6680
|
-
const at = ctx.now().toISOString();
|
|
6681
|
-
const warnings = [];
|
|
6682
|
-
const thread = await ensureStoryThreadForTransition(ctx, lane, target.projectId, target.storyId);
|
|
6683
|
-
lane = thread.lane;
|
|
6684
|
-
if (thread.warning !== undefined)
|
|
6685
|
-
warnings.push(thread.warning);
|
|
6686
|
-
const before = lane;
|
|
6687
|
-
const next = casUpdateLane(hqRoot, laneId, (current) => {
|
|
6688
|
-
assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
|
|
6689
|
-
return updateCarriedStory(current, target.projectId, target.storyId, "in_progress", at);
|
|
6690
|
-
});
|
|
6691
|
-
const transitionWarning = await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_started", "in_progress");
|
|
6692
|
-
if (transitionWarning !== undefined)
|
|
6693
|
-
warnings.push(transitionWarning);
|
|
6694
|
-
const finalLane = readLane(hqRoot, laneId);
|
|
6695
|
-
printJson({ ok: true, action: "story.start", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "in_progress", story_warning: combinedStoryWarning(finalLane, warnings), lane: finalLane });
|
|
6696
|
-
}
|
|
6697
|
-
async function runStoryBlock(ctx, laneId, storyId, opts) {
|
|
6698
|
-
const hqRoot = ctx.hqRoot();
|
|
6699
|
-
let lane = readLane(hqRoot, laneId);
|
|
6700
|
-
assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
|
|
6701
|
-
const target = storyCommandTarget(lane, storyId, opts.project);
|
|
6702
|
-
if (target.story.status === "done") {
|
|
6703
|
-
throw new LanesError("story_already_done", `Story ${target.projectId}/${target.storyId} is done and cannot be blocked again`, {
|
|
6704
|
-
lane_id: laneId,
|
|
6705
|
-
project_id: target.projectId,
|
|
6706
|
-
story_id: target.storyId,
|
|
6707
|
-
});
|
|
6708
|
-
}
|
|
6709
|
-
const reason = opts.reason?.trim();
|
|
6710
|
-
if (!reason)
|
|
6711
|
-
throw new LanesError("missing_block_reason", "--reason is required", {});
|
|
6712
|
-
const at = ctx.now().toISOString();
|
|
6713
|
-
const warnings = [];
|
|
6714
|
-
const thread = await ensureStoryThreadForTransition(ctx, lane, target.projectId, target.storyId);
|
|
6715
|
-
lane = thread.lane;
|
|
6716
|
-
if (thread.warning !== undefined)
|
|
6717
|
-
warnings.push(thread.warning);
|
|
6718
|
-
const before = lane;
|
|
6719
|
-
const next = casUpdateLane(hqRoot, laneId, (current) => {
|
|
6720
|
-
assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
|
|
6721
|
-
return updateCarriedStory(current, target.projectId, target.storyId, "blocked", at, { blocked_reason: reason });
|
|
6722
|
-
});
|
|
6723
|
-
const transitionWarning = await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_blocked", "blocked", { blocked_reason: reason });
|
|
6724
|
-
if (transitionWarning !== undefined)
|
|
6725
|
-
warnings.push(transitionWarning);
|
|
6726
|
-
const finalLane = readLane(hqRoot, laneId);
|
|
6727
|
-
printJson({ ok: true, action: "story.block", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "blocked", blocked_reason: reason, story_warning: combinedStoryWarning(finalLane, warnings), lane: finalLane });
|
|
6728
|
-
}
|
|
6729
|
-
async function runStoryDone(ctx, laneId, storyId, opts) {
|
|
6730
|
-
const hqRoot = ctx.hqRoot();
|
|
6731
|
-
let lane = readLane(hqRoot, laneId);
|
|
6732
|
-
assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
|
|
6733
|
-
if (!opts.verified) {
|
|
6734
|
-
throw new LanesError("senior_verification_required", "Only a senior may mark a story done; pass --verified after reviewing or merging it", { lane_id: laneId });
|
|
6735
|
-
}
|
|
6736
|
-
if (!callerMayVerifyStory(ctx, lane)) {
|
|
6737
|
-
throw new LanesError("senior_verification_required", `Caller is not the senior for lane ${laneId}`, {
|
|
6738
|
-
lane_id: laneId,
|
|
6739
|
-
senior: lane.senior,
|
|
6740
|
-
});
|
|
6741
|
-
}
|
|
6742
|
-
const target = storyCommandTarget(lane, storyId, opts.project);
|
|
6743
|
-
if (target.story.status !== "review") {
|
|
6744
|
-
throw new LanesError("story_not_ready_for_verification", `Story ${target.projectId}/${target.storyId} must be in review before senior verification`, {
|
|
6745
|
-
lane_id: laneId,
|
|
6746
|
-
project_id: target.projectId,
|
|
6747
|
-
story_id: target.storyId,
|
|
6748
|
-
status: target.story.status,
|
|
6749
|
-
});
|
|
6750
|
-
}
|
|
6751
|
-
const at = ctx.now().toISOString();
|
|
6752
|
-
const warnings = [];
|
|
6753
|
-
const thread = await ensureStoryThreadForTransition(ctx, lane, target.projectId, target.storyId);
|
|
6754
|
-
lane = thread.lane;
|
|
6755
|
-
if (thread.warning !== undefined)
|
|
6756
|
-
warnings.push(thread.warning);
|
|
6757
|
-
const before = lane;
|
|
6758
|
-
const next = casUpdateLane(hqRoot, laneId, (current) => {
|
|
6759
|
-
assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
|
|
6760
|
-
return updateCarriedStory(current, target.projectId, target.storyId, "done", at, {
|
|
6761
|
-
verified_at: at,
|
|
6762
|
-
verified_by: current.senior,
|
|
6763
|
-
});
|
|
6764
|
-
});
|
|
6765
|
-
const transitionWarning = await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_done", "done", { summary: "Senior verification complete" });
|
|
6766
|
-
if (transitionWarning !== undefined)
|
|
6767
|
-
warnings.push(transitionWarning);
|
|
6768
|
-
const finalLane = readLane(hqRoot, laneId);
|
|
6769
|
-
printJson({ ok: true, action: "story.done", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "done", verified: true, story_warning: combinedStoryWarning(finalLane, warnings), lane: finalLane });
|
|
6770
|
-
}
|
|
6771
6397
|
/**
|
|
6772
6398
|
* Registration state of the drain hook in the lane's account home, read the
|
|
6773
6399
|
* way the provider reads it. Null when the lane has no bound account yet
|
|
@@ -7208,6 +6834,89 @@ function isDefiniteOutpostLaneNotFound(dispatched, laneId, ownerError) {
|
|
|
7208
6834
|
ownerError?.error === "lane_not_found" &&
|
|
7209
6835
|
ownerError.details.lane_id === laneId;
|
|
7210
6836
|
}
|
|
6837
|
+
async function dispatchPrimaryLaneMessage(ctx, laneId, options) {
|
|
6838
|
+
const outpostId = "primary";
|
|
6839
|
+
const waitMs = outpostSingleLaneWaitMs(ctx.env);
|
|
6840
|
+
let dispatched;
|
|
6841
|
+
try {
|
|
6842
|
+
dispatched = await ctx.dispatchOutpostExec({
|
|
6843
|
+
outpostId,
|
|
6844
|
+
command: buildOutpostLaneCommand({
|
|
6845
|
+
action: "message",
|
|
6846
|
+
laneId,
|
|
6847
|
+
text: options.text,
|
|
6848
|
+
project: options.project,
|
|
6849
|
+
story: options.story,
|
|
6850
|
+
account: options.account,
|
|
6851
|
+
model: options.model,
|
|
6852
|
+
effort: options.effort,
|
|
6853
|
+
worker: options.worker,
|
|
6854
|
+
compact: options.compact,
|
|
6855
|
+
callerLaneId: ctx.env.HQ_LANE_ID?.trim() || undefined,
|
|
6856
|
+
callerSessionId: sessionIdFromEnv(ctx.env),
|
|
6857
|
+
}),
|
|
6858
|
+
timeoutSeconds: Math.ceil(waitMs / 1000),
|
|
6859
|
+
waitMs,
|
|
6860
|
+
});
|
|
6861
|
+
}
|
|
6862
|
+
catch (err) {
|
|
6863
|
+
const message = dispatchErrorMessage(err);
|
|
6864
|
+
throw new LanesError("outpost_dispatch_failed", `Could not dispatch message for lane ${laneId} to primary: ${message}`, { lane_id: laneId, outpost_id: outpostId, wait_ms: waitMs, wait_env: OUTPOST_SINGLE_LANE_WAIT_ENV });
|
|
6865
|
+
}
|
|
6866
|
+
const confirmedCommandId = dispatched.commandId?.trim();
|
|
6867
|
+
const ownerError = ownerErrorFromOutput(dispatched.stdout);
|
|
6868
|
+
if (isDefiniteOutpostLaneNotFound(dispatched, laneId, ownerError)) {
|
|
6869
|
+
throw new LanesError("lane_not_found", ownerError.message, {
|
|
6870
|
+
...ownerError.details,
|
|
6871
|
+
lane_id: laneId,
|
|
6872
|
+
outpost_id: outpostId,
|
|
6873
|
+
command_id: confirmedCommandId,
|
|
6874
|
+
});
|
|
6875
|
+
}
|
|
6876
|
+
if (dispatched.done === true &&
|
|
6877
|
+
Boolean(confirmedCommandId) &&
|
|
6878
|
+
dispatched.status === "Failed" &&
|
|
6879
|
+
dispatched.exitCode !== null &&
|
|
6880
|
+
dispatched.exitCode !== 0 &&
|
|
6881
|
+
ownerError) {
|
|
6882
|
+
throw new LanesError(ownerError.error, ownerError.message, {
|
|
6883
|
+
...ownerError.details,
|
|
6884
|
+
lane_id: laneId,
|
|
6885
|
+
outpost_id: outpostId,
|
|
6886
|
+
command_id: confirmedCommandId,
|
|
6887
|
+
});
|
|
6888
|
+
}
|
|
6889
|
+
if (dispatched.done !== true ||
|
|
6890
|
+
dispatched.status !== "Success" ||
|
|
6891
|
+
dispatched.exitCode !== 0 ||
|
|
6892
|
+
!confirmedCommandId) {
|
|
6893
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Primary did not confirm message delivery for lane ${laneId}`, {
|
|
6894
|
+
lane_id: laneId,
|
|
6895
|
+
outpost_id: outpostId,
|
|
6896
|
+
command_id: confirmedCommandId ?? null,
|
|
6897
|
+
status: dispatched.status,
|
|
6898
|
+
done: dispatched.done ?? false,
|
|
6899
|
+
exit_code: dispatched.exitCode,
|
|
6900
|
+
});
|
|
6901
|
+
}
|
|
6902
|
+
const remote = parseRemoteJson(dispatched.stdout, "Primary lane message");
|
|
6903
|
+
if (remote === null ||
|
|
6904
|
+
remote.ok !== true ||
|
|
6905
|
+
remote.action !== "message" ||
|
|
6906
|
+
remote.lane_id !== laneId ||
|
|
6907
|
+
remote.accepted !== true) {
|
|
6908
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Primary completed exec without a confirmed message result for lane ${laneId}`, { lane_id: laneId, outpost_id: outpostId, command_id: confirmedCommandId });
|
|
6909
|
+
}
|
|
6910
|
+
if (!options.silent) {
|
|
6911
|
+
printJson({
|
|
6912
|
+
...remote,
|
|
6913
|
+
outpost_id: outpostId,
|
|
6914
|
+
outpost_command_id: confirmedCommandId,
|
|
6915
|
+
outpost_transport: "outposts_exec",
|
|
6916
|
+
delivered_to: { host: outpostId, lane_id: laneId, mechanism: "outposts_exec" },
|
|
6917
|
+
});
|
|
6918
|
+
}
|
|
6919
|
+
}
|
|
7211
6920
|
async function dispatchOutpostLaneStop(ctx, lane, reason, quiet = false) {
|
|
7212
6921
|
if (lane.host.kind !== "outpost") {
|
|
7213
6922
|
throw new LanesError("outpost_dispatch_failed", `Lane ${lane.lane_id} has no Outpost host to dispatch stop to`, { lane_id: lane.lane_id });
|
|
@@ -7882,6 +7591,7 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
|
|
|
7882
7591
|
? { model: automatic.preservedModel, model_source: "preserved" }
|
|
7883
7592
|
: {}),
|
|
7884
7593
|
},
|
|
7594
|
+
accountSelection: selection,
|
|
7885
7595
|
};
|
|
7886
7596
|
}
|
|
7887
7597
|
let latest;
|
|
@@ -7899,7 +7609,10 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
|
|
|
7899
7609
|
latest.last_resume?.session_id !== lane.last_resume?.session_id)
|
|
7900
7610
|
return { lane: latest };
|
|
7901
7611
|
if (selection.account === lane.account) {
|
|
7902
|
-
return {
|
|
7612
|
+
return {
|
|
7613
|
+
lane: persistClaudeAccountSelection(hqRoot, recoveredLane, selection),
|
|
7614
|
+
accountSelection: selection,
|
|
7615
|
+
};
|
|
7903
7616
|
}
|
|
7904
7617
|
try {
|
|
7905
7618
|
const moved = await prepareAccountMove(ctx, recoveredLane, {
|
|
@@ -7913,7 +7626,7 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
|
|
|
7913
7626
|
admissionLocksHeld: true,
|
|
7914
7627
|
accountSelection: selection,
|
|
7915
7628
|
});
|
|
7916
|
-
return { lane: moved.lane };
|
|
7629
|
+
return { lane: moved.lane, accountSelection: selection };
|
|
7917
7630
|
}
|
|
7918
7631
|
catch (err) {
|
|
7919
7632
|
let latest;
|
|
@@ -8052,6 +7765,19 @@ function attachJsonNoop(cmd) {
|
|
|
8052
7765
|
}
|
|
8053
7766
|
export function registerLanesCommand(parent, deps = {}) {
|
|
8054
7767
|
const ctx = resolveDeps(deps);
|
|
7768
|
+
const storyHandlers = createLaneStoryHandlers({
|
|
7769
|
+
assertLaneRunsHereForOperatorMutation,
|
|
7770
|
+
casUpdateLane,
|
|
7771
|
+
combinedStoryWarning,
|
|
7772
|
+
emitLaneMeshEvent,
|
|
7773
|
+
ensureLaneThreadRecord,
|
|
7774
|
+
printJson,
|
|
7775
|
+
readLane,
|
|
7776
|
+
storyBoardCheck,
|
|
7777
|
+
storyNotOnBoardWarning,
|
|
7778
|
+
updateLane,
|
|
7779
|
+
validateProjectStories,
|
|
7780
|
+
});
|
|
8055
7781
|
const accountHandlers = createLaneAccountsHandlers({
|
|
8056
7782
|
automaticBalanceMoveHistoryReason,
|
|
8057
7783
|
ensureDir,
|
|
@@ -8069,6 +7795,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8069
7795
|
MAX_PLACEMENT_OUTPOSTS,
|
|
8070
7796
|
OUTPOST_QUESTION_LOOKUP_CONCURRENCY,
|
|
8071
7797
|
assertLaneRunsHereForOperatorMutation,
|
|
7798
|
+
assertQuestionAnswerAuthorized,
|
|
8072
7799
|
assertQuestionCompanyMatchesLane,
|
|
8073
7800
|
casUpdateQuestion,
|
|
8074
7801
|
effectiveLaneQuestionCompany,
|
|
@@ -8107,6 +7834,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8107
7834
|
assessLaneAdmission,
|
|
8108
7835
|
casUpdateLane,
|
|
8109
7836
|
dispatchOutpostLaneCommand,
|
|
7837
|
+
dispatchPrimaryLaneMessage,
|
|
8110
7838
|
drainHookStatusFor,
|
|
8111
7839
|
laneIsHostedHere,
|
|
8112
7840
|
laneWithRecoveredProviderSession,
|
|
@@ -8207,6 +7935,11 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8207
7935
|
}, wrap);
|
|
8208
7936
|
registerOutpostPrunerCommand(lanes);
|
|
8209
7937
|
registerLaneKpisCommand(lanes, { hqRoot: ctx.hqRoot, now: ctx.now, wrap });
|
|
7938
|
+
registerLaneAutopilotReportCommand(lanes, {
|
|
7939
|
+
hqRoot: ctx.hqRoot,
|
|
7940
|
+
now: ctx.now,
|
|
7941
|
+
currentHost: () => resolveAdmissionHost({ hostname: ctx.hostname, readOutpostIdentity: ctx.readOutpostIdentity }),
|
|
7942
|
+
}, wrap);
|
|
8210
7943
|
const noStoryOption = new Option("--no-story <reason>", "Create a lane without a story and record why");
|
|
8211
7944
|
noStoryOption.attributeName = () => "noStory";
|
|
8212
7945
|
lanes
|
|
@@ -8418,7 +8151,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8418
8151
|
.description("List the stories carried by a lane")
|
|
8419
8152
|
.argument("<lane-id>", "Lane id")
|
|
8420
8153
|
.action(wrap(async (laneId) => {
|
|
8421
|
-
await runStoryList(ctx, laneId);
|
|
8154
|
+
await storyHandlers.runStoryList(ctx, laneId);
|
|
8422
8155
|
}));
|
|
8423
8156
|
stories
|
|
8424
8157
|
.command("add")
|
|
@@ -8427,7 +8160,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8427
8160
|
.argument("<story-id>", "Story id")
|
|
8428
8161
|
.option("--project <slug>", "Project id; defaults to the lane's current project")
|
|
8429
8162
|
.action(wrap(async (laneId, storyId, opts) => {
|
|
8430
|
-
await runStoryAdd(ctx, laneId, storyId, opts);
|
|
8163
|
+
await storyHandlers.runStoryAdd(ctx, laneId, storyId, opts);
|
|
8431
8164
|
}));
|
|
8432
8165
|
stories
|
|
8433
8166
|
.command("start")
|
|
@@ -8436,7 +8169,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8436
8169
|
.argument("<story-id>", "Story id")
|
|
8437
8170
|
.option("--project <slug>", "Project id; defaults to the lane's current project")
|
|
8438
8171
|
.action(wrap(async (laneId, storyId, opts) => {
|
|
8439
|
-
await runStoryStart(ctx, laneId, storyId, opts);
|
|
8172
|
+
await storyHandlers.runStoryStart(ctx, laneId, storyId, opts);
|
|
8440
8173
|
}));
|
|
8441
8174
|
stories
|
|
8442
8175
|
.command("block")
|
|
@@ -8446,7 +8179,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8446
8179
|
.requiredOption("--reason <text>", "Why the story is blocked")
|
|
8447
8180
|
.option("--project <slug>", "Project id; defaults to the lane's current project")
|
|
8448
8181
|
.action(wrap(async (laneId, storyId, opts) => {
|
|
8449
|
-
await runStoryBlock(ctx, laneId, storyId, opts);
|
|
8182
|
+
await storyHandlers.runStoryBlock(ctx, laneId, storyId, opts);
|
|
8450
8183
|
}));
|
|
8451
8184
|
stories
|
|
8452
8185
|
.command("done")
|
|
@@ -8456,7 +8189,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8456
8189
|
.requiredOption("--verified", "Confirm senior review or merge")
|
|
8457
8190
|
.option("--project <slug>", "Project id; defaults to the lane's current project")
|
|
8458
8191
|
.action(wrap(async (laneId, storyId, opts) => {
|
|
8459
|
-
await runStoryDone(ctx, laneId, storyId, opts);
|
|
8192
|
+
await storyHandlers.runStoryDone(ctx, laneId, storyId, opts);
|
|
8460
8193
|
}));
|
|
8461
8194
|
lanes
|
|
8462
8195
|
.command("message")
|
|
@@ -8745,9 +8478,11 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8745
8478
|
.action(wrap(async (opts) => {
|
|
8746
8479
|
await runPickup(ctx, opts);
|
|
8747
8480
|
}));
|
|
8748
|
-
lanes
|
|
8749
|
-
|
|
8750
|
-
|
|
8481
|
+
const autopilot = lanes.commands.find((command) => command.name() === "autopilot");
|
|
8482
|
+
if (!autopilot)
|
|
8483
|
+
throw new Error("Lanes autopilot command group was not registered");
|
|
8484
|
+
autopilot
|
|
8485
|
+
.description("Run lead autopilot checks and report follow-through metrics")
|
|
8751
8486
|
.command("run")
|
|
8752
8487
|
.description("Run one lead autopilot check pass")
|
|
8753
8488
|
.option("--dry-run", "Report proposed wakes without writing them")
|