@indigoai-us/hq-cli 5.282.0 → 5.284.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 +32 -0
- package/assets/scaffold/.claude/hooks/validate-policy-frontmatter.sh +485 -0
- package/assets/scaffold/core/scripts/eval-trigger.sh +193 -0
- package/assets/scaffold/core/scripts/policy-age-report.sh +156 -0
- package/assets/scaffold/core/scripts/policy-retire.sh +199 -0
- package/assets/scaffold/core/scripts/read-policy-frontmatter.sh +32 -0
- package/dist/command-catalog.generated.d.ts +67 -0
- package/dist/command-catalog.generated.js +83 -0
- package/dist/commands/core-knowledge-prefs.d.ts +10 -0
- package/dist/commands/core-knowledge-prefs.js +125 -0
- package/dist/commands/core-native-policy-flag.d.ts +9 -0
- package/dist/commands/core-native-policy-flag.js +12 -0
- package/dist/commands/core-ontology-native.d.ts +0 -1
- package/dist/commands/core-ontology-native.js +2 -30
- package/dist/commands/core-ontology.d.ts +4 -0
- package/dist/commands/core-ontology.js +8 -3
- package/dist/commands/core-policy-frontmatter.d.ts +8 -0
- package/dist/commands/core-policy-frontmatter.js +52 -0
- package/dist/commands/core-policy.d.ts +15 -0
- package/dist/commands/core-policy.js +982 -0
- package/dist/commands/core.js +14 -1
- package/dist/commands/lanes-shared.d.ts +4 -1
- package/dist/commands/lanes-shared.js +2 -1
- package/dist/commands/lanes.d.ts +8 -0
- package/dist/commands/lanes.js +303 -91
- package/dist/fast-lanes-list-route.d.ts +8 -0
- package/dist/fast-lanes-list-route.js +30 -0
- package/dist/fast-lanes-list.d.ts +3 -0
- package/dist/fast-lanes-list.js +252 -0
- package/dist/fast-scaffold.d.ts +5 -0
- package/dist/fast-scaffold.js +10 -0
- package/dist/index.js +10 -1
- package/dist/lib/checkpoint-ontology/merge-copies.d.ts +24 -1
- package/dist/lib/checkpoint-ontology/merge-copies.js +70 -1
- package/dist/lib/lanes/flag-keys.d.ts +3 -0
- package/dist/lib/lanes/flag-keys.js +3 -0
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/outpost-dispatch.d.ts +9 -1
- package/dist/lib/lanes/outpost-dispatch.js +14 -2
- package/dist/lib/lanes/outpost-hire-cache.d.ts +3 -0
- package/dist/lib/lanes/outpost-hire-cache.js +11 -3
- package/dist/lib/lanes/outpost-snapshot-cache.d.ts +26 -0
- package/dist/lib/lanes/outpost-snapshot-cache.js +215 -0
- package/dist/lib/lanes/store.d.ts +4 -0
- package/dist/lib/lanes/store.js +39 -0
- package/dist/lib/mesh/live/daemon/credentials.js +8 -0
- package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +1 -0
- package/dist/lib/mesh/live/daemon/work-subscribe.js +143 -5
- package/dist/lib/policy-validator.d.ts +2 -0
- package/dist/lib/policy-validator.js +136 -0
- package/dist/native-fast-route.d.ts +2 -0
- package/dist/native-fast-route.js +6 -2
- package/dist/utils/contribution-table.d.ts +1 -1
- package/package.json +1 -1
package/dist/commands/lanes.js
CHANGED
|
@@ -31,13 +31,14 @@ import { withCliFeatureUsed } from "../utils/cli-telemetry.js";
|
|
|
31
31
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
32
32
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
33
33
|
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
34
|
+
import { getOrFetchOutpostSnapshot, OUTPOST_SNAPSHOT_CACHE_TTL_MS, outpostLaneListSnapshotKey, OutpostSnapshotInvocationMemo, } from "../lib/lanes/outpost-snapshot-cache.js";
|
|
34
35
|
import { createOutpostHireGrantContext, parseOutpostHireGrantContext, selectOutpostHireGrantHierarchy, sameHireGrantSenior, } from "../lib/lanes/hire-grant-dispatch.js";
|
|
35
36
|
import { createOutpostProfileRepinContext, parseOutpostProfileRepinContext, } from "../lib/lanes/outpost-profile-repin.js";
|
|
36
37
|
import { createOutpostRegistryWorkerApprovalContext, parseOutpostRegistryWorkerApprovalContext, } from "../lib/lanes/outpost-registry-worker-approval.js";
|
|
37
38
|
import { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity, resolveAdmissionHost, } from "../lib/lanes/admission-host.js";
|
|
38
39
|
export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
39
|
-
import { buildOutpostCapacityCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand, buildOutpostQuestionListCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, buildOutpostLaneListCommand, outpostRejectedEffortOption, parseOutpostPickupPayload, PICKUP_WAIT_MS, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
|
|
40
|
-
import { outpostHireSummaryCacheKey, readOutpostHireSummaryCache, writeOutpostHireSummaryCache, } from "../lib/lanes/outpost-hire-cache.js";
|
|
40
|
+
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";
|
|
41
|
+
import { outpostHireSummaryCacheKey, LANE_OUTPOST_SNAPSHOT_CACHE_FLAG, OUTPOST_HIRE_SUMMARY_CACHE_TTL_MS, readOutpostHireSummaryCache, readOutpostHireSummaryCacheIncludingStale, writeOutpostHireSummaryCache, } from "../lib/lanes/outpost-hire-cache.js";
|
|
41
42
|
import { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "../lib/lanes/host-capacity.js";
|
|
42
43
|
import { BALANCE_MOVABLE_STATES, selectClaudeAccount, } from "../lib/lanes/claude-balance.js";
|
|
43
44
|
import { parseUserYaml } from "../utils/user-yaml-error.js";
|
|
@@ -51,7 +52,7 @@ import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, lan
|
|
|
51
52
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
52
53
|
import { listLaneReviews } from "../lib/lanes/reviews.js";
|
|
53
54
|
import { effectiveAccountHome } from "../lib/lanes/account-home.js";
|
|
54
|
-
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, 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";
|
|
55
|
+
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";
|
|
55
56
|
import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
|
|
56
57
|
import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
|
|
57
58
|
import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
|
|
@@ -79,6 +80,7 @@ export const LANE_REMOTE_MIRROR_SYNC_FLAG = "lanes.remote-done-mirror";
|
|
|
79
80
|
const REMOTE_MIRROR_REFRESH_BATCH_SIZE = 100;
|
|
80
81
|
/** The lane → mesh seam. Defined in lib/lanes/mesh-events.ts; the watcher shares it. */
|
|
81
82
|
export { emitLaneDecisionEvents };
|
|
83
|
+
export { LANE_OUTPOST_SNAPSHOT_CACHE_FLAG };
|
|
82
84
|
function withoutProviderHome(value) {
|
|
83
85
|
if (Array.isArray(value))
|
|
84
86
|
return value.map(withoutProviderHome);
|
|
@@ -294,6 +296,8 @@ function resolveDeps(deps = {}) {
|
|
|
294
296
|
})),
|
|
295
297
|
fetchText: deps.fetchText ?? defaultFetchText,
|
|
296
298
|
lanePlacementEnabled: deps.lanePlacementEnabled ?? defaultLanePlacementEnabled,
|
|
299
|
+
outpostSnapshotCacheEnabled: deps.outpostSnapshotCacheEnabled ?? (() => readLanesRegistryFlag(LANE_OUTPOST_SNAPSHOT_CACHE_FLAG, { env })),
|
|
300
|
+
startOutpostLaneSnapshotRefresh: deps.startOutpostLaneSnapshotRefresh ?? ((root) => startOutpostLaneSnapshotRefresh(root, env)),
|
|
297
301
|
storyLedgerEnabled: deps.storyLedgerEnabled ?? laneStoryLedgerEnabled,
|
|
298
302
|
isSsmOwnerDispatch: deps.isSsmOwnerDispatch ?? (() => isSsmRunCommandProcess()),
|
|
299
303
|
withGrantHireLocks: deps.withGrantHireLocks ?? withLaneHireLocks,
|
|
@@ -460,7 +464,7 @@ function readQuestionBody(hqRoot, question) {
|
|
|
460
464
|
return stored.body;
|
|
461
465
|
}
|
|
462
466
|
function loadAllLanes(hqRoot) {
|
|
463
|
-
return
|
|
467
|
+
return readAllLanes(hqRoot);
|
|
464
468
|
}
|
|
465
469
|
function laneHireFields(lane, lanes, aliveChildrenByParent) {
|
|
466
470
|
const aliveChildCount = aliveChildrenByParent
|
|
@@ -697,7 +701,7 @@ function laneListQueryFlags(query) {
|
|
|
697
701
|
return [
|
|
698
702
|
...(query.laneIds?.length ? ["--lane"] : []),
|
|
699
703
|
...(query.active ? ["--active"] : []),
|
|
700
|
-
...(query.hierarchy
|
|
704
|
+
...(query.hierarchy?.length ? ["--hierarchy"] : []),
|
|
701
705
|
...(query.snapshot ? ["--snapshot"] : []),
|
|
702
706
|
];
|
|
703
707
|
}
|
|
@@ -726,7 +730,7 @@ async function dispatchOutpostLaneList(ctx, outpostId, query, timeoutSeconds, wa
|
|
|
726
730
|
waitMs,
|
|
727
731
|
});
|
|
728
732
|
}
|
|
729
|
-
async function fetchOutpostListHireSummaries(ctx, outpostId, lanes) {
|
|
733
|
+
async function fetchOutpostListHireSummaries(ctx, outpostId, lanes, localLanes) {
|
|
730
734
|
const fallback = (error) => new Map(lanes.map((lane) => [lane.lane_id, {
|
|
731
735
|
error: new LanesError(error.error, error.message, {
|
|
732
736
|
...error.details,
|
|
@@ -734,65 +738,91 @@ async function fetchOutpostListHireSummaries(ctx, outpostId, lanes) {
|
|
|
734
738
|
lane_id: lane.lane_id,
|
|
735
739
|
}),
|
|
736
740
|
}]));
|
|
737
|
-
|
|
741
|
+
const summaryReadError = (err) => {
|
|
742
|
+
if (isLanesError(err)) {
|
|
743
|
+
if (err.error === "outpost_lane_list_invalid") {
|
|
744
|
+
return new LanesError("outpost_lane_hire_summary_invalid", err.message, err.details);
|
|
745
|
+
}
|
|
746
|
+
if (err.error === "outpost_lane_list_unavailable") {
|
|
747
|
+
return new LanesError("outpost_lane_hire_summary_unavailable", err.message, err.details);
|
|
748
|
+
}
|
|
749
|
+
return err;
|
|
750
|
+
}
|
|
751
|
+
return new LanesError("outpost_lane_hire_summary_unavailable", `Could not read hire summaries from Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { outpost_id: outpostId });
|
|
752
|
+
};
|
|
753
|
+
const dispatchOptions = {
|
|
754
|
+
timeoutSeconds: OUTPOST_HIRE_SUMMARY_TIMEOUT_SECONDS,
|
|
755
|
+
waitMs: OUTPOST_HIRE_SUMMARY_WAIT_MS,
|
|
756
|
+
};
|
|
757
|
+
let ownerLanes;
|
|
738
758
|
try {
|
|
739
|
-
|
|
759
|
+
ownerLanes = await readOutpostLaneRecords(ctx, outpostId, {
|
|
760
|
+
hierarchy: lanes.map((lane) => lane.lane_id),
|
|
761
|
+
snapshot: true,
|
|
762
|
+
}, dispatchOptions);
|
|
740
763
|
}
|
|
741
764
|
catch (err) {
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
const
|
|
750
|
-
if (
|
|
751
|
-
|
|
765
|
+
return fallback(summaryReadError(err));
|
|
766
|
+
}
|
|
767
|
+
const requestedOwnerIds = new Set(lanes.map((lane) => lane.lane_id));
|
|
768
|
+
const returnedOwnerIds = new Set(ownerLanes
|
|
769
|
+
.filter((lane) => requestedOwnerIds.has(lane.lane_id))
|
|
770
|
+
.map((lane) => lane.lane_id));
|
|
771
|
+
const missingOwnerLanes = lanes.filter((lane) => !returnedOwnerIds.has(lane.lane_id));
|
|
772
|
+
const retryErrors = new Map();
|
|
773
|
+
if (returnedOwnerIds.size > 0 && missingOwnerLanes.length > 0) {
|
|
774
|
+
const retries = await mapWithConcurrency(missingOwnerLanes, OUTPOST_HIRE_SUMMARY_CONCURRENCY, async (lane) => {
|
|
775
|
+
try {
|
|
776
|
+
const rows = await readOutpostLaneRecords(ctx, outpostId, {
|
|
777
|
+
hierarchy: [lane.lane_id],
|
|
778
|
+
snapshot: true,
|
|
779
|
+
}, dispatchOptions);
|
|
780
|
+
return { laneId: lane.lane_id, rows };
|
|
781
|
+
}
|
|
782
|
+
catch (err) {
|
|
783
|
+
return { laneId: lane.lane_id, error: summaryReadError(err) };
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
for (const retry of retries) {
|
|
787
|
+
if (retry.error)
|
|
788
|
+
retryErrors.set(retry.laneId, retry.error);
|
|
789
|
+
else if (retry.rows)
|
|
790
|
+
ownerLanes.push(...retry.rows);
|
|
791
|
+
}
|
|
752
792
|
}
|
|
793
|
+
const hierarchy = mergeOwnerLaneHierarchy(localLanes, ownerLanes);
|
|
753
794
|
const outcomes = new Map();
|
|
754
795
|
for (const lane of lanes) {
|
|
755
|
-
const
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
outcomes.set(lane.lane_id, { error
|
|
796
|
+
const matchingLanes = ownerLanes.filter((ownerLane) => ownerLane.lane_id === lane.lane_id);
|
|
797
|
+
if (matchingLanes.length === 0) {
|
|
798
|
+
const error = retryErrors.get(lane.lane_id) ?? new LanesError("outpost_lane_hire_summary_missing", `Outpost ${outpostId} did not return lane ${lane.lane_id} in its lane list`, { outpost_id: outpostId, lane_id: lane.lane_id });
|
|
799
|
+
outcomes.set(lane.lane_id, { error });
|
|
759
800
|
continue;
|
|
760
801
|
}
|
|
761
|
-
if (
|
|
762
|
-
outcomes.set(lane.lane_id, { error: new LanesError("outpost_lane_hire_summary_invalid", `Outpost ${outpostId} returned duplicate lane rows for ${lane.lane_id}`, { outpost_id: outpostId, lane_id: lane.lane_id
|
|
802
|
+
if (matchingLanes.length !== 1) {
|
|
803
|
+
outcomes.set(lane.lane_id, { error: new LanesError("outpost_lane_hire_summary_invalid", `Outpost ${outpostId} returned duplicate lane rows for ${lane.lane_id}`, { outpost_id: outpostId, lane_id: lane.lane_id }) });
|
|
763
804
|
continue;
|
|
764
805
|
}
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
outcomes.set(lane.lane_id, {
|
|
768
|
-
|
|
769
|
-
laneId: lane.lane_id,
|
|
770
|
-
rawLane: row,
|
|
771
|
-
hireGrant: row.hire_grant,
|
|
772
|
-
hireAliveChildren: row.hire_alive_children,
|
|
773
|
-
hire: row.hire,
|
|
774
|
-
hireSummary: row.hire_summary,
|
|
775
|
-
commandId: dispatched.commandId,
|
|
776
|
-
}) });
|
|
777
|
-
}
|
|
778
|
-
catch (err) {
|
|
779
|
-
outcomes.set(lane.lane_id, { error: isLanesError(err)
|
|
780
|
-
? err
|
|
781
|
-
: new LanesError("outpost_lane_hire_summary_invalid", `Outpost ${outpostId} returned an invalid hire summary for lane ${lane.lane_id}`, { outpost_id: outpostId, lane_id: lane.lane_id, command_id: dispatched.commandId }) });
|
|
806
|
+
const ownerLane = matchingLanes[0];
|
|
807
|
+
if (ownerLane.host.kind !== "outpost" || ownerLane.host.id !== outpostId) {
|
|
808
|
+
outcomes.set(lane.lane_id, { error: new LanesError("outpost_lane_hire_summary_invalid", `Outpost ${outpostId} returned a lane owned by a different host`, { outpost_id: outpostId, lane_id: lane.lane_id }) });
|
|
809
|
+
continue;
|
|
782
810
|
}
|
|
811
|
+
outcomes.set(lane.lane_id, { fields: laneHireFields(ownerLane, hierarchy) });
|
|
783
812
|
}
|
|
784
813
|
return outcomes;
|
|
785
814
|
}
|
|
786
|
-
async function batchOwnerLaneHireFields(ctx, lanes) {
|
|
815
|
+
async function batchOwnerLaneHireFields(ctx, lanes, localLanes, options = {}) {
|
|
787
816
|
const outcomes = new Map();
|
|
788
817
|
const groups = new Map();
|
|
789
818
|
const nowMs = ctx.now().getTime();
|
|
790
|
-
const cached =
|
|
819
|
+
const cached = readOutpostHireSummaryCacheIncludingStale(ctx.hqRoot(), nowMs);
|
|
791
820
|
for (const lane of lanes) {
|
|
792
821
|
if (lane.host.kind !== "outpost")
|
|
793
822
|
continue;
|
|
794
823
|
const cachedSummary = cached.get(outpostHireSummaryCacheKey(lane.host.id, lane.lane_id));
|
|
795
|
-
if (cachedSummary
|
|
824
|
+
if (cachedSummary && !options.forceRefresh &&
|
|
825
|
+
nowMs - Date.parse(cachedSummary.cached_at) <= OUTPOST_HIRE_SUMMARY_CACHE_TTL_MS) {
|
|
796
826
|
outcomes.set(lane.lane_id, {
|
|
797
827
|
fields: {
|
|
798
828
|
hire_grant: cachedSummary.hire_grant,
|
|
@@ -807,7 +837,7 @@ async function batchOwnerLaneHireFields(ctx, lanes) {
|
|
|
807
837
|
group.push(lane);
|
|
808
838
|
groups.set(lane.host.id, group);
|
|
809
839
|
}
|
|
810
|
-
const fetchedGroups = await mapWithConcurrency([...groups.entries()], OUTPOST_HIRE_SUMMARY_CONCURRENCY, ([outpostId, group]) => fetchOutpostListHireSummaries(ctx, outpostId, group));
|
|
840
|
+
const fetchedGroups = await mapWithConcurrency([...groups.entries()], OUTPOST_HIRE_SUMMARY_CONCURRENCY, ([outpostId, group]) => fetchOutpostListHireSummaries(ctx, outpostId, group, localLanes));
|
|
811
841
|
const cacheUpdates = [];
|
|
812
842
|
for (const groupOutcomes of fetchedGroups) {
|
|
813
843
|
for (const [laneId, outcome] of groupOutcomes) {
|
|
@@ -947,6 +977,9 @@ function laneHostFromCtx(ctx, expectedId) {
|
|
|
947
977
|
return { kind: "local", id: hostname };
|
|
948
978
|
}
|
|
949
979
|
async function readOutpostCapacity(ctx, outpostId) {
|
|
980
|
+
return readOutpostSnapshotWithCache(ctx, JSON.stringify(["capacity", outpostId]), (value) => isOutpostCapacitySnapshot(value, outpostId), () => readOutpostCapacityUncached(ctx, outpostId));
|
|
981
|
+
}
|
|
982
|
+
async function readOutpostCapacityUncached(ctx, outpostId) {
|
|
950
983
|
let result;
|
|
951
984
|
try {
|
|
952
985
|
result = await ctx.dispatchOutpostExec({
|
|
@@ -980,7 +1013,68 @@ async function readOutpostCapacity(ctx, outpostId) {
|
|
|
980
1013
|
}
|
|
981
1014
|
return snapshot;
|
|
982
1015
|
}
|
|
983
|
-
async function
|
|
1016
|
+
async function readOutpostSnapshotWithCache(ctx, key, isValue, fetch) {
|
|
1017
|
+
let enabled = false;
|
|
1018
|
+
try {
|
|
1019
|
+
enabled = await ctx.outpostSnapshotCacheEnabled();
|
|
1020
|
+
}
|
|
1021
|
+
catch (err) {
|
|
1022
|
+
process.stderr.write(`lanes snapshot cache flag read failed; using uncached Outpost read: ${dispatchErrorMessage(err)}\n`);
|
|
1023
|
+
}
|
|
1024
|
+
if (!enabled)
|
|
1025
|
+
return fetch();
|
|
1026
|
+
return getOrFetchOutpostSnapshot({ hqRoot: ctx.hqRoot(), key, isValue, fetch });
|
|
1027
|
+
}
|
|
1028
|
+
function startOutpostLaneSnapshotRefresh(root, env) {
|
|
1029
|
+
let entry;
|
|
1030
|
+
try {
|
|
1031
|
+
const currentEntry = process.argv[1];
|
|
1032
|
+
if (!currentEntry)
|
|
1033
|
+
throw new Error("process.argv[1] is empty");
|
|
1034
|
+
entry = fs.realpathSync(currentEntry);
|
|
1035
|
+
}
|
|
1036
|
+
catch (err) {
|
|
1037
|
+
process.stderr.write(`lanes snapshot refresh could not resolve the running CLI entry: ${dispatchErrorMessage(err)}\n`);
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
try {
|
|
1041
|
+
const child = spawn(process.execPath, [...process.execArgv, entry, "lanes", "list"], {
|
|
1042
|
+
cwd: root,
|
|
1043
|
+
detached: true,
|
|
1044
|
+
stdio: ["ignore", "ignore", "inherit"],
|
|
1045
|
+
env: { ...env, HQ_ROOT: root, HQ_NO_UPDATE_CHECK: "1", HQ_INTERNAL_LANES_REFRESH_REMOTE_SNAPSHOT: "1" },
|
|
1046
|
+
});
|
|
1047
|
+
child.once("error", (err) => {
|
|
1048
|
+
process.stderr.write(`lanes snapshot refresh could not start: ${dispatchErrorMessage(err)}\n`);
|
|
1049
|
+
});
|
|
1050
|
+
child.unref();
|
|
1051
|
+
}
|
|
1052
|
+
catch (err) {
|
|
1053
|
+
process.stderr.write(`lanes snapshot refresh could not start: ${dispatchErrorMessage(err)}\n`);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
function isLaneRecordArray(value) {
|
|
1057
|
+
if (!Array.isArray(value))
|
|
1058
|
+
return false;
|
|
1059
|
+
try {
|
|
1060
|
+
for (const lane of value)
|
|
1061
|
+
parseLaneRecord(lane);
|
|
1062
|
+
return true;
|
|
1063
|
+
}
|
|
1064
|
+
catch {
|
|
1065
|
+
return false;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
function isOutpostCapacitySnapshot(value, outpostId) {
|
|
1069
|
+
try {
|
|
1070
|
+
const snapshot = parseHostCapacitySnapshot(value);
|
|
1071
|
+
return snapshot.host.kind === "outpost" && snapshot.host.id === outpostId;
|
|
1072
|
+
}
|
|
1073
|
+
catch {
|
|
1074
|
+
return false;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
async function loadHostCapacitySnapshots(ctx, hqRoot, config = loadLanesConfig(hqRoot), localLanes = loadAllLanes(hqRoot)) {
|
|
984
1078
|
const localHost = laneHostFromCtx(ctx);
|
|
985
1079
|
let outpostIds;
|
|
986
1080
|
try {
|
|
@@ -1007,7 +1101,7 @@ async function loadHostCapacitySnapshots(ctx, hqRoot, config = loadLanesConfig(h
|
|
|
1007
1101
|
.sort();
|
|
1008
1102
|
const local = buildHostCapacitySnapshot({
|
|
1009
1103
|
host: localHost,
|
|
1010
|
-
lanes:
|
|
1104
|
+
lanes: localLanes,
|
|
1011
1105
|
config,
|
|
1012
1106
|
existsSync: ctx.existsSync,
|
|
1013
1107
|
hostname: ctx.hostname(),
|
|
@@ -1053,25 +1147,30 @@ function normalizedLaneListQuery(query) {
|
|
|
1053
1147
|
const laneIds = query.laneIds === undefined
|
|
1054
1148
|
? undefined
|
|
1055
1149
|
: [...new Set(query.laneIds)].sort((a, b) => a.localeCompare(b));
|
|
1150
|
+
const hierarchy = query.hierarchy === undefined
|
|
1151
|
+
? undefined
|
|
1152
|
+
: [...new Set(query.hierarchy)].sort((a, b) => a.localeCompare(b));
|
|
1056
1153
|
return {
|
|
1057
1154
|
...(laneIds === undefined ? {} : { laneIds }),
|
|
1058
1155
|
...(query.active ? { active: true } : {}),
|
|
1059
|
-
...(
|
|
1156
|
+
...(hierarchy === undefined ? {} : { hierarchy }),
|
|
1060
1157
|
...(query.snapshot ? { snapshot: true } : {}),
|
|
1061
1158
|
...(query.strictSnapshot ? { strictSnapshot: true } : {}),
|
|
1062
1159
|
};
|
|
1063
1160
|
}
|
|
1064
|
-
async function readOutpostLaneRecordsUncached(ctx, outpostId, query) {
|
|
1161
|
+
async function readOutpostLaneRecordsUncached(ctx, outpostId, query, dispatchOptions = {}) {
|
|
1065
1162
|
const targetedLookup = query.hierarchy !== undefined || query.laneIds !== undefined;
|
|
1066
1163
|
const waitMs = targetedLookup ? outpostSingleLaneWaitMs(ctx.env) : CAPACITY_PROBE_TIMEOUT_MS;
|
|
1164
|
+
const dispatchWaitMs = dispatchOptions.waitMs ?? waitMs;
|
|
1165
|
+
const dispatchTimeoutSeconds = dispatchOptions.timeoutSeconds ?? Math.ceil(dispatchWaitMs / 1000);
|
|
1067
1166
|
let result;
|
|
1068
1167
|
try {
|
|
1069
|
-
result = await dispatchOutpostLaneList(ctx, outpostId, query,
|
|
1168
|
+
result = await dispatchOutpostLaneList(ctx, outpostId, query, dispatchTimeoutSeconds, dispatchWaitMs);
|
|
1070
1169
|
}
|
|
1071
1170
|
catch (err) {
|
|
1072
1171
|
const message = dispatchErrorMessage(err);
|
|
1073
1172
|
if (/timed out waiting for outpost pickup/i.test(message)) {
|
|
1074
|
-
throw new LanesError("outpost_lane_list_unavailable", outpostPickupTimeoutMessage(outpostId,
|
|
1173
|
+
throw new LanesError("outpost_lane_list_unavailable", outpostPickupTimeoutMessage(outpostId, dispatchWaitMs, "reading the requested lane"), { outpost_id: outpostId, wait_ms: dispatchWaitMs, wait_env: OUTPOST_SINGLE_LANE_WAIT_ENV });
|
|
1075
1174
|
}
|
|
1076
1175
|
if (isLanesError(err))
|
|
1077
1176
|
throw err;
|
|
@@ -1130,24 +1229,14 @@ async function readOutpostLaneRecordsUncached(ctx, outpostId, query) {
|
|
|
1130
1229
|
}
|
|
1131
1230
|
return lanes;
|
|
1132
1231
|
}
|
|
1133
|
-
async function readOutpostLaneRecords(ctx, outpostId, requestedQuery = {}) {
|
|
1232
|
+
async function readOutpostLaneRecords(ctx, outpostId, requestedQuery = {}, dispatchOptions = {}) {
|
|
1134
1233
|
const query = normalizedLaneListQuery(requestedQuery);
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
outpostLaneListMemo.set(ctx, byOutpost);
|
|
1234
|
+
let invocationMemo = outpostLaneListMemo.get(ctx);
|
|
1235
|
+
if (!invocationMemo) {
|
|
1236
|
+
invocationMemo = new OutpostSnapshotInvocationMemo();
|
|
1237
|
+
outpostLaneListMemo.set(ctx, invocationMemo);
|
|
1140
1238
|
}
|
|
1141
|
-
|
|
1142
|
-
if (existing) {
|
|
1143
|
-
if (existing.queryKey !== queryKey) {
|
|
1144
|
-
throw new LanesError("outpost_lane_list_query_conflict", `Outpost ${outpostId} was already read with a different lane-list query in this invocation`, { outpost_id: outpostId });
|
|
1145
|
-
}
|
|
1146
|
-
return existing.promise;
|
|
1147
|
-
}
|
|
1148
|
-
const promise = readOutpostLaneRecordsUncached(ctx, outpostId, query);
|
|
1149
|
-
byOutpost.set(outpostId, { queryKey, promise });
|
|
1150
|
-
return promise;
|
|
1239
|
+
return invocationMemo.getOrCreate(outpostId, query, () => readOutpostSnapshotWithCache(ctx, outpostLaneListSnapshotKey(outpostId, query), isLaneRecordArray, () => readOutpostLaneRecordsUncached(ctx, outpostId, query, dispatchOptions)));
|
|
1151
1240
|
}
|
|
1152
1241
|
/** Refresh only primary-created, non-terminal Outpost mirrors in one bounded owner query per Outpost. */
|
|
1153
1242
|
async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds) {
|
|
@@ -1307,6 +1396,16 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
|
|
|
1307
1396
|
if (!(err instanceof LanesError) || err.error !== "lane_not_found")
|
|
1308
1397
|
throw err;
|
|
1309
1398
|
}
|
|
1399
|
+
const requestedUlid = laneUlidPart(laneId);
|
|
1400
|
+
if (requestedUlid) {
|
|
1401
|
+
const localMatches = listLaneIds(ctx.hqRoot()).filter((storedId) => laneUlidPart(storedId) === requestedUlid);
|
|
1402
|
+
if (localMatches.length > 1) {
|
|
1403
|
+
throw new LanesError("lane_ulid_ambiguous", `More than one local lane record has ULID ${requestedUlid}`, { lane_id: laneId, matches: localMatches });
|
|
1404
|
+
}
|
|
1405
|
+
const localMatch = localMatches[0];
|
|
1406
|
+
if (localMatch)
|
|
1407
|
+
return { lane: readLane(ctx.hqRoot(), localMatch), remoteOnly: false };
|
|
1408
|
+
}
|
|
1310
1409
|
const separator = laneId.lastIndexOf("_");
|
|
1311
1410
|
const hostSuffix = separator < 0 ? "" : laneId.slice(separator + 1);
|
|
1312
1411
|
if (!hostSuffix || sanitizeHostComponent(hostSuffix) !== hostSuffix) {
|
|
@@ -1373,7 +1472,7 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
|
|
|
1373
1472
|
let ownerLanes;
|
|
1374
1473
|
try {
|
|
1375
1474
|
ownerLanes = await readOutpostLaneRecords(ctx, outpostId, {
|
|
1376
|
-
hierarchy: laneId,
|
|
1475
|
+
hierarchy: [laneId],
|
|
1377
1476
|
snapshot: true,
|
|
1378
1477
|
});
|
|
1379
1478
|
}
|
|
@@ -1407,6 +1506,13 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
|
|
|
1407
1506
|
ownerHost: match.lane.host,
|
|
1408
1507
|
};
|
|
1409
1508
|
}
|
|
1509
|
+
function laneUlidPart(laneId) {
|
|
1510
|
+
const separator = laneId.indexOf("_");
|
|
1511
|
+
if (separator !== 26)
|
|
1512
|
+
return undefined;
|
|
1513
|
+
const ulid = laneId.slice(0, separator);
|
|
1514
|
+
return /^[0-9A-HJKMNP-TV-Z]{26}$/i.test(ulid) ? ulid.toUpperCase() : undefined;
|
|
1515
|
+
}
|
|
1410
1516
|
function commandLaneNotFound(laneId, outpostHint, details = {}) {
|
|
1411
1517
|
const hint = outpostHint
|
|
1412
1518
|
? "; pass --outpost <id> to confirm a registered Outpost"
|
|
@@ -3802,7 +3908,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
|
|
|
3802
3908
|
let grantHierarchy;
|
|
3803
3909
|
if (action === "grant") {
|
|
3804
3910
|
const hierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneRecords(ctx, outpostId, {
|
|
3805
|
-
hierarchy: lane.lane_id,
|
|
3911
|
+
hierarchy: [lane.lane_id],
|
|
3806
3912
|
snapshot: true,
|
|
3807
3913
|
}));
|
|
3808
3914
|
grantHierarchy = hierarchy;
|
|
@@ -3811,7 +3917,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
|
|
|
3811
3917
|
else if ((action === "resume" && lane.senior.kind === "lane") ||
|
|
3812
3918
|
(action === "message" && (lane.senior.kind === "lane" || lane.senior.kind === "session"))) {
|
|
3813
3919
|
const hierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneRecords(ctx, outpostId, {
|
|
3814
|
-
hierarchy: lane.lane_id,
|
|
3920
|
+
hierarchy: [lane.lane_id],
|
|
3815
3921
|
snapshot: true,
|
|
3816
3922
|
}));
|
|
3817
3923
|
grantContext = createOutpostHireGrantContext(lane, hierarchy, action === "message" ? options.supersededMessageCallerId : undefined, action === "message" ? options.supersededMessageSessionId : undefined);
|
|
@@ -4359,7 +4465,7 @@ async function runPickup(ctx, opts) {
|
|
|
4359
4465
|
lane: started,
|
|
4360
4466
|
});
|
|
4361
4467
|
}
|
|
4362
|
-
async function runList(ctx, opts) {
|
|
4468
|
+
async function runList(ctx, opts, fastRouteFlags) {
|
|
4363
4469
|
if (opts.json && opts.human) {
|
|
4364
4470
|
throw new LanesError("conflicting_output_format", "Use either --json or --human for hq lanes list", { json: true, human: true });
|
|
4365
4471
|
}
|
|
@@ -4369,8 +4475,11 @@ async function runList(ctx, opts) {
|
|
|
4369
4475
|
new Set(requestedLaneIds).size !== requestedLaneIds.length) {
|
|
4370
4476
|
throw new LanesError("lane_list_filter_invalid", "--lane must be repeated with distinct, non-empty lane ids", { lane_ids: requestedLaneIds });
|
|
4371
4477
|
}
|
|
4372
|
-
if (opts.hierarchy !== undefined &&
|
|
4373
|
-
|
|
4478
|
+
if (opts.hierarchy !== undefined &&
|
|
4479
|
+
(!Array.isArray(opts.hierarchy) || opts.hierarchy.length === 0 ||
|
|
4480
|
+
opts.hierarchy.some((laneId) => typeof laneId !== "string" || !laneId.trim() || laneId.trim() !== laneId) ||
|
|
4481
|
+
new Set(opts.hierarchy).size !== opts.hierarchy.length)) {
|
|
4482
|
+
throw new LanesError("lane_list_hierarchy_invalid", "--hierarchy must be repeated with distinct, non-empty lane ids without surrounding whitespace", {});
|
|
4374
4483
|
}
|
|
4375
4484
|
if (opts.snapshot && (opts.human || opts.company || opts.project || opts.story || opts.state || opts.senior)) {
|
|
4376
4485
|
throw new LanesError("lane_list_snapshot_invalid", "--snapshot only supports owner lane filters and JSON output", {});
|
|
@@ -4395,7 +4504,7 @@ async function runList(ctx, opts) {
|
|
|
4395
4504
|
: loadAllLanes(hqRoot);
|
|
4396
4505
|
const hierarchyIds = opts.hierarchy === undefined
|
|
4397
4506
|
? undefined
|
|
4398
|
-
: new Set(
|
|
4507
|
+
: new Set(opts.hierarchy.flatMap((laneId) => selectOutpostHireGrantHierarchy(laneId, laneSource).map((lane) => lane.lane_id)));
|
|
4399
4508
|
let selectedLanes = laneSource
|
|
4400
4509
|
.filter((lane) => {
|
|
4401
4510
|
if (requestedLaneIds.length > 0 && !requestedIds.has(lane.lane_id))
|
|
@@ -4434,9 +4543,12 @@ async function runList(ctx, opts) {
|
|
|
4434
4543
|
});
|
|
4435
4544
|
return;
|
|
4436
4545
|
}
|
|
4437
|
-
const
|
|
4438
|
-
|
|
4439
|
-
|
|
4546
|
+
const remoteRead = ctx.env[REMOTE_LANE_READ_ENV] === "1";
|
|
4547
|
+
const remoteRefresh = remoteRead
|
|
4548
|
+
? { updated: new Map(), unverifiedRemote: new Map() }
|
|
4549
|
+
: await refreshRemoteLaneMirrors(ctx, hqRoot, laneSource, new Set(selectedLanes
|
|
4550
|
+
.filter((lane) => !isTerminalLaneState(lane.state))
|
|
4551
|
+
.map((lane) => lane.lane_id)));
|
|
4440
4552
|
for (const [laneId, updated] of remoteRefresh.updated) {
|
|
4441
4553
|
const index = laneSource.findIndex((lane) => lane.lane_id === laneId);
|
|
4442
4554
|
if (index >= 0)
|
|
@@ -4454,7 +4566,8 @@ async function runList(ctx, opts) {
|
|
|
4454
4566
|
}
|
|
4455
4567
|
const ownerEligibleLaneIds = new Set();
|
|
4456
4568
|
for (const lane of selectedLanes) {
|
|
4457
|
-
if (
|
|
4569
|
+
if (remoteRead ||
|
|
4570
|
+
lane.host.kind !== "outpost" ||
|
|
4458
4571
|
isTerminalLaneState(lane.state) ||
|
|
4459
4572
|
laneRunsHere(lane, host)) {
|
|
4460
4573
|
continue;
|
|
@@ -4462,7 +4575,33 @@ async function runList(ctx, opts) {
|
|
|
4462
4575
|
ownerEligibleLaneIds.add(lane.lane_id);
|
|
4463
4576
|
}
|
|
4464
4577
|
const ownerLanes = selectedLanes.filter((lane) => ownerEligibleLaneIds.has(lane.lane_id));
|
|
4578
|
+
const refreshRemote = ctx.env.HQ_INTERNAL_LANES_REFRESH_REMOTE_SNAPSHOT === "1";
|
|
4579
|
+
let snapshotCacheEnabled = fastRouteFlags?.snapshotCacheEnabled ?? false;
|
|
4580
|
+
if (fastRouteFlags === undefined &&
|
|
4581
|
+
(refreshRemote || (!opts.fresh && ownerLanes.length > 0))) {
|
|
4582
|
+
try {
|
|
4583
|
+
snapshotCacheEnabled = await ctx.outpostSnapshotCacheEnabled();
|
|
4584
|
+
}
|
|
4585
|
+
catch (err) {
|
|
4586
|
+
process.stderr.write(`lanes snapshot cache flag read failed; using wait-for-remote list: ${dispatchErrorMessage(err)}\n`);
|
|
4587
|
+
}
|
|
4588
|
+
}
|
|
4589
|
+
if (refreshRemote) {
|
|
4590
|
+
if (snapshotCacheEnabled && ownerLanes.length > 0) {
|
|
4591
|
+
const outcomes = await batchOwnerLaneHireFields(ctx, ownerLanes, allLanes, { forceRefresh: true });
|
|
4592
|
+
const failures = [...outcomes.entries()].filter(([, outcome]) => "error" in outcome);
|
|
4593
|
+
if (failures.length > 0) {
|
|
4594
|
+
const firstOutcome = failures[0][1];
|
|
4595
|
+
const firstError = "error" in firstOutcome ? firstOutcome.error : undefined;
|
|
4596
|
+
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`);
|
|
4597
|
+
throw firstError ?? new LanesError("outpost_lane_hire_summary_missing", "Background Outpost lane summary refresh returned a missing outcome", { lane_ids: failures.map(([laneId]) => laneId) });
|
|
4598
|
+
}
|
|
4599
|
+
}
|
|
4600
|
+
return;
|
|
4601
|
+
}
|
|
4465
4602
|
const hireByLane = new Map();
|
|
4603
|
+
const remoteSnapshotMetadata = new Map();
|
|
4604
|
+
let remoteLanesLoading = false;
|
|
4466
4605
|
for (const lane of selectedLanes) {
|
|
4467
4606
|
if (!ownerEligibleLaneIds.has(lane.lane_id)) {
|
|
4468
4607
|
hireByLane.set(lane.lane_id, {
|
|
@@ -4472,8 +4611,48 @@ async function runList(ctx, opts) {
|
|
|
4472
4611
|
continue;
|
|
4473
4612
|
}
|
|
4474
4613
|
}
|
|
4475
|
-
if (ownerLanes.length > 0) {
|
|
4476
|
-
const
|
|
4614
|
+
if (snapshotCacheEnabled && !opts.fresh && ownerLanes.length > 0) {
|
|
4615
|
+
const nowMs = ctx.now().getTime();
|
|
4616
|
+
const cached = readOutpostHireSummaryCacheIncludingStale(hqRoot, nowMs);
|
|
4617
|
+
let refreshNeeded = false;
|
|
4618
|
+
for (const lane of ownerLanes) {
|
|
4619
|
+
if (lane.host.kind !== "outpost")
|
|
4620
|
+
continue;
|
|
4621
|
+
const summary = cached.get(outpostHireSummaryCacheKey(lane.host.id, lane.lane_id));
|
|
4622
|
+
if (!summary) {
|
|
4623
|
+
hireByLane.set(lane.lane_id, {
|
|
4624
|
+
...laneHireFields(lane, allLanes, aliveChildrenByParent),
|
|
4625
|
+
hire_source: "local",
|
|
4626
|
+
});
|
|
4627
|
+
remoteSnapshotMetadata.set(lane.lane_id, { ageSeconds: null, stale: true, loading: true });
|
|
4628
|
+
refreshNeeded = true;
|
|
4629
|
+
remoteLanesLoading = true;
|
|
4630
|
+
continue;
|
|
4631
|
+
}
|
|
4632
|
+
const ageMs = Math.max(0, nowMs - Date.parse(summary.cached_at));
|
|
4633
|
+
const stale = ageMs >= OUTPOST_SNAPSHOT_CACHE_TTL_MS;
|
|
4634
|
+
hireByLane.set(lane.lane_id, {
|
|
4635
|
+
hire_grant: summary.hire_grant,
|
|
4636
|
+
hire_alive_children: summary.hire_alive_children,
|
|
4637
|
+
hire: summary.hire,
|
|
4638
|
+
hire_summary: summary.hire_summary,
|
|
4639
|
+
hire_source: "owner",
|
|
4640
|
+
});
|
|
4641
|
+
remoteSnapshotMetadata.set(lane.lane_id, {
|
|
4642
|
+
ageSeconds: Math.floor(ageMs / 1_000),
|
|
4643
|
+
stale,
|
|
4644
|
+
loading: stale,
|
|
4645
|
+
});
|
|
4646
|
+
if (stale) {
|
|
4647
|
+
refreshNeeded = true;
|
|
4648
|
+
remoteLanesLoading = true;
|
|
4649
|
+
}
|
|
4650
|
+
}
|
|
4651
|
+
if (refreshNeeded)
|
|
4652
|
+
ctx.startOutpostLaneSnapshotRefresh(hqRoot);
|
|
4653
|
+
}
|
|
4654
|
+
else if (ownerLanes.length > 0) {
|
|
4655
|
+
const ownerSummaries = await batchOwnerLaneHireFields(ctx, ownerLanes, allLanes);
|
|
4477
4656
|
for (const lane of ownerLanes) {
|
|
4478
4657
|
const outcome = ownerSummaries.get(lane.lane_id);
|
|
4479
4658
|
if (outcome && "fields" in outcome) {
|
|
@@ -4506,6 +4685,15 @@ async function runList(ctx, opts) {
|
|
|
4506
4685
|
...(remoteQueued
|
|
4507
4686
|
? { state: "remote", remote_state: lane.state, queued_reason: undefined, queued_details: undefined }
|
|
4508
4687
|
: {}),
|
|
4688
|
+
...(remoteSnapshotMetadata.has(lane.lane_id)
|
|
4689
|
+
? {
|
|
4690
|
+
remote_snapshot_age_s: remoteSnapshotMetadata.get(lane.lane_id).ageSeconds,
|
|
4691
|
+
remote_snapshot_stale: remoteSnapshotMetadata.get(lane.lane_id).stale,
|
|
4692
|
+
...(remoteSnapshotMetadata.get(lane.lane_id).loading
|
|
4693
|
+
? { remote_snapshot_loading: true }
|
|
4694
|
+
: {}),
|
|
4695
|
+
}
|
|
4696
|
+
: {}),
|
|
4509
4697
|
overdue: Boolean(lane.overdue) || isOverdue(hqRoot, lane.lane_id, now),
|
|
4510
4698
|
stalled: Boolean(lane.stalled),
|
|
4511
4699
|
// Listing is observational. Reconcile owns durable liveness transitions.
|
|
@@ -4529,8 +4717,10 @@ async function runList(ctx, opts) {
|
|
|
4529
4717
|
};
|
|
4530
4718
|
});
|
|
4531
4719
|
const placementCompany = company ?? processLanePlacementCompany(ctx);
|
|
4532
|
-
const
|
|
4533
|
-
|
|
4720
|
+
const placementEnabled = !remoteRead && (fastRouteFlags?.lanePlacementEnabled ??
|
|
4721
|
+
await readLanePlacementEnabled(ctx, placementCompany));
|
|
4722
|
+
const host_capacity = placementEnabled
|
|
4723
|
+
? (await loadHostCapacitySnapshots(ctx, hqRoot, undefined, allLanes)).entries
|
|
4534
4724
|
: undefined;
|
|
4535
4725
|
const result = {
|
|
4536
4726
|
ok: true,
|
|
@@ -4539,24 +4729,44 @@ async function runList(ctx, opts) {
|
|
|
4539
4729
|
...(remoteRefresh.unverifiedRemote.size > 0
|
|
4540
4730
|
? { unverified_remote_lanes: [...remoteRefresh.unverifiedRemote.keys()] }
|
|
4541
4731
|
: {}),
|
|
4732
|
+
...(remoteLanesLoading ? { remote_lanes_loading: true } : {}),
|
|
4542
4733
|
...(host_capacity !== undefined ? { host_capacity } : {}),
|
|
4543
4734
|
};
|
|
4544
4735
|
if (opts.human)
|
|
4545
|
-
console.log(formatLaneListHuman(lanes));
|
|
4736
|
+
console.log(formatLaneListHuman(lanes, remoteLanesLoading));
|
|
4546
4737
|
else
|
|
4547
4738
|
printJson(result);
|
|
4548
4739
|
}
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4740
|
+
/** Keeps the placement-enabled cache-first path on the full lane command graph. */
|
|
4741
|
+
export async function runFastCachedLanesListFromEntrypoint() {
|
|
4742
|
+
try {
|
|
4743
|
+
await runList(resolveDeps(), { json: true }, {
|
|
4744
|
+
snapshotCacheEnabled: true,
|
|
4745
|
+
lanePlacementEnabled: true,
|
|
4746
|
+
});
|
|
4747
|
+
}
|
|
4748
|
+
catch (err) {
|
|
4749
|
+
failJson(err);
|
|
4750
|
+
}
|
|
4751
|
+
}
|
|
4752
|
+
function formatLaneListHuman(lanes, remoteLanesLoading = false) {
|
|
4753
|
+
const rows = lanes.map((lane) => {
|
|
4553
4754
|
const ids = lane.story_ids;
|
|
4554
4755
|
const storyLabel = ids.length > 0
|
|
4555
4756
|
? "stories: " + ids.join(", ")
|
|
4556
4757
|
: "no story: " + (lane.no_story_reason ?? "reason not recorded");
|
|
4758
|
+
const snapshotNote = lane.remote_snapshot_stale && lane.remote_snapshot_age_s !== null &&
|
|
4759
|
+
lane.remote_snapshot_age_s !== undefined
|
|
4760
|
+
? ` remote snapshot stale (${lane.remote_snapshot_age_s}s)`
|
|
4761
|
+
: "";
|
|
4557
4762
|
return lane.lane_id + " " + lane.state + " " + lane.provider + " " + lane.project_id +
|
|
4558
|
-
" " + storyLabel;
|
|
4559
|
-
})
|
|
4763
|
+
" " + storyLabel + snapshotNote;
|
|
4764
|
+
});
|
|
4765
|
+
if (rows.length === 0)
|
|
4766
|
+
rows.push("No lanes.");
|
|
4767
|
+
if (remoteLanesLoading)
|
|
4768
|
+
rows.push("Remote lanes loading.");
|
|
4769
|
+
return rows.join("\n");
|
|
4560
4770
|
}
|
|
4561
4771
|
async function runStatus(ctx, opts) {
|
|
4562
4772
|
if (!ctx.storyLedgerEnabled()) {
|
|
@@ -5238,7 +5448,7 @@ async function runGrant(ctx, laneId, opts) {
|
|
|
5238
5448
|
await dispatchOutpostLaneCommand(ctx, initial, "grant", { hire: String(hireGrant) });
|
|
5239
5449
|
return;
|
|
5240
5450
|
}
|
|
5241
|
-
const updated = await applyHireGrant(ctx,
|
|
5451
|
+
const updated = await applyHireGrant(ctx, initial.lane_id, hireGrant, "senior");
|
|
5242
5452
|
printGrantResult(hqRoot, updated);
|
|
5243
5453
|
}
|
|
5244
5454
|
async function runApprove(ctx, laneId, opts) {
|
|
@@ -7765,8 +7975,10 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
7765
7975
|
.option("--senior <ref>", "Filter by senior ref")
|
|
7766
7976
|
.option("--lane <id>", "Filter by lane id (repeatable)", (value, previous = []) => [...previous, value], [])
|
|
7767
7977
|
.option("--active", "Include only lanes that are not terminal")
|
|
7768
|
-
.addOption(new Option("--hierarchy <lane-id>").hideHelp()
|
|
7978
|
+
.addOption(new Option("--hierarchy <lane-id>").hideHelp()
|
|
7979
|
+
.argParser((value, previous = []) => [...previous, value]))
|
|
7769
7980
|
.addOption(new Option("--snapshot").hideHelp())
|
|
7981
|
+
.option("--fresh", "Wait for fresh remote owner summaries")
|
|
7770
7982
|
.option("--json", "Emit JSON on stdout (default)")
|
|
7771
7983
|
.option("--human", "Emit human-readable output")
|
|
7772
7984
|
.action(wrap(async (opts) => {
|