@indigoai-us/hq-cli 5.292.0 → 5.294.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 +40 -0
- package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +5 -1
- package/dist/command-catalog.generated.d.ts +45 -24
- package/dist/command-catalog.generated.js +57 -30
- 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/lanes-autopilot.d.ts +11 -0
- package/dist/commands/lanes-autopilot.js +336 -0
- package/dist/commands/lanes-move.js +54 -1
- package/dist/commands/lanes-questions.js +12 -5
- package/dist/commands/lanes-shared.d.ts +17 -0
- package/dist/commands/lanes-stories.d.ts +14 -0
- package/dist/commands/lanes-stories.js +308 -0
- package/dist/commands/lanes.d.ts +2 -0
- package/dist/commands/lanes.js +158 -314
- package/dist/fast-core.js +53 -25
- package/dist/fast-scaffold.d.ts +15 -2
- package/dist/fast-scaffold.js +167 -7
- package/dist/lib/core-utils/sentry-report.js +2 -2
- 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/daemon/run.d.ts +11 -0
- package/dist/lib/daemon/run.js +17 -1
- package/dist/lib/lanes/autopilot-metrics.d.ts +99 -0
- package/dist/lib/lanes/autopilot-metrics.js +301 -0
- package/dist/lib/lanes/autopilot.d.ts +13 -1
- package/dist/lib/lanes/autopilot.js +271 -28
- package/dist/lib/lanes/grants.d.ts +1 -1
- package/dist/lib/lanes/grants.js +7 -5
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.d.ts +5 -0
- package/dist/lib/lanes/keep-alive.js +9 -4
- package/dist/lib/lanes/questions.d.ts +2 -1
- package/dist/lib/lanes/questions.js +2 -0
- package/dist/lib/lanes/reconcile.js +186 -9
- package/dist/lib/lanes/spawn.js +4 -0
- package/dist/lib/lanes/store.d.ts +2 -0
- package/dist/lib/lanes/store.js +53 -4
- package/dist/lib/lanes/types.d.ts +13 -1
- package/dist/lib/lanes/types.js +2 -2
- package/dist/lib/lanes/watcher.js +3 -1
- package/dist/native-fast-route.d.ts +3 -1
- package/dist/native-fast-route.js +10 -8
- 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.d.ts +7 -0
- package/dist/utils/self-update.js +5 -1
- package/dist/utils/version-gate.d.ts +15 -0
- package/dist/utils/version-gate.js +46 -2
- package/package.json +1 -1
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";
|
|
@@ -55,7 +57,7 @@ import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
|
55
57
|
import { listLaneReviews } from "../lib/lanes/reviews.js";
|
|
56
58
|
import { effectiveAccountHome } from "../lib/lanes/account-home.js";
|
|
57
59
|
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 {
|
|
60
|
+
import { laneStories, storyWarning, } from "../lib/lanes/stories.js";
|
|
59
61
|
import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
|
|
60
62
|
import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
|
|
61
63
|
import { doneKeepAliveVerdict, hasPendingKeepAliveWork, keepAliveExclusion, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
|
|
@@ -490,6 +492,67 @@ function mergeOwnerLaneHierarchy(supplied, ownerLanes) {
|
|
|
490
492
|
byId.set(lane.lane_id, lane);
|
|
491
493
|
return [...byId.values()];
|
|
492
494
|
}
|
|
495
|
+
function hierarchyWithRemoteSenior(hierarchy, remoteSenior) {
|
|
496
|
+
return [
|
|
497
|
+
...hierarchy.filter((lane) => lane.lane_id !== remoteSenior.lane_id),
|
|
498
|
+
remoteSenior,
|
|
499
|
+
];
|
|
500
|
+
}
|
|
501
|
+
async function readRemoteHireGrantSenior(ctx, hqRoot, seniorLaneId) {
|
|
502
|
+
if (!listLaneIds(hqRoot).includes(seniorLaneId))
|
|
503
|
+
return undefined;
|
|
504
|
+
const mirror = readLane(hqRoot, seniorLaneId);
|
|
505
|
+
if (mirror.host.kind !== "outpost" ||
|
|
506
|
+
laneRunsHere(mirror, reconcileHostFromCtx(ctx))) {
|
|
507
|
+
return undefined;
|
|
508
|
+
}
|
|
509
|
+
const outpostId = mirror.host.id;
|
|
510
|
+
const unavailable = (reason) => new LanesError("hire_grant_senior_unavailable", `Cannot verify senior lane ${seniorLaneId} from owning Outpost ${outpostId}; refusing hire-grant admission (${reason})`, { senior_lane_id: seniorLaneId, outpost_id: outpostId, reason });
|
|
511
|
+
let ownerRecords;
|
|
512
|
+
try {
|
|
513
|
+
ownerRecords = await readOutpostLaneRecords(ctx, outpostId, {
|
|
514
|
+
laneIds: [seniorLaneId],
|
|
515
|
+
snapshot: true,
|
|
516
|
+
strictSnapshot: true,
|
|
517
|
+
}, {}, { bypassCache: true });
|
|
518
|
+
}
|
|
519
|
+
catch (error) {
|
|
520
|
+
throw unavailable(isLanesError(error) ? error.error : "outpost_lane_list_unavailable");
|
|
521
|
+
}
|
|
522
|
+
const matches = ownerRecords.filter((lane) => lane.lane_id === seniorLaneId);
|
|
523
|
+
const owner = matches[0];
|
|
524
|
+
const ownerIsHostedByOutpost = owner !== undefined && ((owner.host.kind === "outpost" && owner.host.id === outpostId) ||
|
|
525
|
+
(owner.host.kind === "local" && owner.host.id.trim() !== ""));
|
|
526
|
+
if (matches.length !== 1 || owner === undefined || !ownerIsHostedByOutpost) {
|
|
527
|
+
throw unavailable(matches.length === 0
|
|
528
|
+
? "owner_did_not_return_senior"
|
|
529
|
+
: matches.length > 1
|
|
530
|
+
? "owner_returned_duplicate_seniors"
|
|
531
|
+
: "owner_senior_identity_mismatch");
|
|
532
|
+
}
|
|
533
|
+
try {
|
|
534
|
+
const mirrorCompany = resolveLaneCompany(hqRoot, mirror).company;
|
|
535
|
+
const ownerCompany = resolveLaneCompany(hqRoot, owner).company;
|
|
536
|
+
if ((ownerCompany?.uid ?? null) !== (mirrorCompany?.uid ?? null) ||
|
|
537
|
+
(ownerCompany?.slug ?? null) !== (mirrorCompany?.slug ?? null)) {
|
|
538
|
+
throw unavailable("owner_company_did_not_match_mirror");
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
if (isLanesError(error) && error.error === "hire_grant_senior_unavailable")
|
|
543
|
+
throw error;
|
|
544
|
+
throw unavailable("owner_company_identity_unavailable");
|
|
545
|
+
}
|
|
546
|
+
return owner;
|
|
547
|
+
}
|
|
548
|
+
async function hierarchyWithVerifiedRemoteSenior(ctx, hqRoot, lane, hierarchy) {
|
|
549
|
+
if (lane.senior.kind !== "lane" || !lane.senior.id)
|
|
550
|
+
return [...hierarchy];
|
|
551
|
+
const remoteSenior = await readRemoteHireGrantSenior(ctx, hqRoot, lane.senior.id);
|
|
552
|
+
return remoteSenior === undefined
|
|
553
|
+
? [...hierarchy]
|
|
554
|
+
: mergeOwnerLaneRecords(hierarchy, [remoteSenior]);
|
|
555
|
+
}
|
|
493
556
|
function mergeOwnerLaneRecords(supplied, ownerLanes) {
|
|
494
557
|
const byId = new Map(supplied.map((lane) => [lane.lane_id, lane]));
|
|
495
558
|
for (const lane of ownerLanes)
|
|
@@ -911,8 +974,18 @@ async function withResumeHireAdmission(ctx, lane, fn, grantContext) {
|
|
|
911
974
|
throw new LanesError("hire_grant_context_required", `Resume of Outpost lane ${lane.lane_id} requires the primary senior's hire-grant context`, { lane_id: lane.lane_id, senior_lane_id: lane.senior.id });
|
|
912
975
|
}
|
|
913
976
|
await refreshRemoteLaneMirrors(ctx, hqRoot, loadAllLanes(hqRoot), new Set([lane.senior.id]));
|
|
977
|
+
const remoteSenior = grantContext === undefined
|
|
978
|
+
? await readRemoteHireGrantSenior(ctx, hqRoot, lane.senior.id)
|
|
979
|
+
: undefined;
|
|
980
|
+
const suppliedHierarchy = grantContext?.hierarchy ?? (remoteSenior === undefined
|
|
981
|
+
? undefined
|
|
982
|
+
: hierarchyWithRemoteSenior(loadAllLanes(hqRoot), remoteSenior));
|
|
914
983
|
return withHireGrantAdmission(hqRoot, lane.senior.id, (snapshot) => {
|
|
915
984
|
const current = readLane(hqRoot, lane.lane_id);
|
|
985
|
+
if (remoteSenior !== undefined &&
|
|
986
|
+
(current.senior.kind !== "lane" || current.senior.id !== remoteSenior.lane_id)) {
|
|
987
|
+
throw new LanesError("hire_grant_senior_changed", `Lane ${current.lane_id}'s senior changed while its owning Outpost was being checked`, { lane_id: current.lane_id, expected_senior_lane_id: remoteSenior.lane_id, current_senior: current.senior });
|
|
988
|
+
}
|
|
916
989
|
if (grantContext !== undefined &&
|
|
917
990
|
!sameHireGrantSenior(current.senior, grantContext.expected_senior)) {
|
|
918
991
|
throw new LanesError("hire_grant_senior_changed", `Lane ${current.lane_id}'s senior changed before the Outpost resumed it`, {
|
|
@@ -929,7 +1002,7 @@ async function withResumeHireAdmission(ctx, lane, fn, grantContext) {
|
|
|
929
1002
|
if (refusal !== null)
|
|
930
1003
|
throw refusal;
|
|
931
1004
|
return fn();
|
|
932
|
-
},
|
|
1005
|
+
}, suppliedHierarchy, remoteSenior === undefined ? undefined : new Set([remoteSenior.lane_id]));
|
|
933
1006
|
}
|
|
934
1007
|
const CAPACITY_PROBE_TIMEOUT_MS = 15_000;
|
|
935
1008
|
const MAX_PLACEMENT_OUTPOSTS = 16;
|
|
@@ -1205,8 +1278,11 @@ async function readOutpostLaneRecordsUncached(ctx, outpostId, query, dispatchOpt
|
|
|
1205
1278
|
}
|
|
1206
1279
|
return lanes;
|
|
1207
1280
|
}
|
|
1208
|
-
async function readOutpostLaneRecords(ctx, outpostId, requestedQuery = {}, dispatchOptions = {}) {
|
|
1281
|
+
async function readOutpostLaneRecords(ctx, outpostId, requestedQuery = {}, dispatchOptions = {}, options = {}) {
|
|
1209
1282
|
const query = normalizedLaneListQuery(requestedQuery);
|
|
1283
|
+
if (options.bypassCache === true) {
|
|
1284
|
+
return readOutpostLaneRecordsUncached(ctx, outpostId, query, dispatchOptions);
|
|
1285
|
+
}
|
|
1210
1286
|
let invocationMemo = outpostLaneListMemo.get(ctx);
|
|
1211
1287
|
if (!invocationMemo) {
|
|
1212
1288
|
invocationMemo = new OutpostSnapshotInvocationMemo();
|
|
@@ -2144,6 +2220,10 @@ function assertKnownOutpost(requested, known) {
|
|
|
2144
2220
|
*/
|
|
2145
2221
|
export const ASK_BACKOFF_MS = [5_000, 15_000];
|
|
2146
2222
|
export const ASK_ATTEMPTS = ASK_BACKOFF_MS.length + 1;
|
|
2223
|
+
/** Flip to true in the next release to make --priority mandatory. */
|
|
2224
|
+
export const ASK_PRIORITY_REQUIRED = false;
|
|
2225
|
+
const ASK_PRIORITY_WARNING = "--priority will be required in the next release.";
|
|
2226
|
+
const QUESTION_PRIORITIES = ["P0", "P1", "P2"];
|
|
2147
2227
|
function withCasRetry(fn, attempts = 5) {
|
|
2148
2228
|
let last;
|
|
2149
2229
|
for (let i = 0; i < attempts; i += 1) {
|
|
@@ -3980,19 +4060,21 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
|
|
|
3980
4060
|
let grantContext;
|
|
3981
4061
|
let grantHierarchy;
|
|
3982
4062
|
if (action === "grant") {
|
|
3983
|
-
const
|
|
4063
|
+
const ownerHierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneRecords(ctx, outpostId, {
|
|
3984
4064
|
hierarchy: [lane.lane_id],
|
|
3985
4065
|
snapshot: true,
|
|
3986
4066
|
}));
|
|
4067
|
+
const hierarchy = await hierarchyWithVerifiedRemoteSenior(ctx, ctx.hqRoot(), lane, ownerHierarchy);
|
|
3987
4068
|
grantHierarchy = hierarchy;
|
|
3988
4069
|
grantContext = createOutpostHireGrantContext(lane, hierarchy);
|
|
3989
4070
|
}
|
|
3990
4071
|
else if ((action === "resume" && lane.senior.kind === "lane") ||
|
|
3991
4072
|
(action === "message" && (lane.senior.kind === "lane" || lane.senior.kind === "session"))) {
|
|
3992
|
-
const
|
|
4073
|
+
const ownerHierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneRecords(ctx, outpostId, {
|
|
3993
4074
|
hierarchy: [lane.lane_id],
|
|
3994
4075
|
snapshot: true,
|
|
3995
4076
|
}));
|
|
4077
|
+
const hierarchy = await hierarchyWithVerifiedRemoteSenior(ctx, ctx.hqRoot(), lane, ownerHierarchy);
|
|
3996
4078
|
grantContext = createOutpostHireGrantContext(lane, hierarchy, action === "message" ? options.supersededMessageCallerId : undefined, action === "message" ? options.supersededMessageSessionId : undefined);
|
|
3997
4079
|
}
|
|
3998
4080
|
let dispatched;
|
|
@@ -5215,6 +5297,13 @@ function appendCallerSeniorReason(message, authorization) {
|
|
|
5215
5297
|
? message
|
|
5216
5298
|
: `${message}; authorization refused: ${callerSeniorReason(authorization)}`;
|
|
5217
5299
|
}
|
|
5300
|
+
function assertQuestionAnswerAuthorized(ctx, lane) {
|
|
5301
|
+
const authorization = callerSeniorAuthorization(ctx, lane.senior);
|
|
5302
|
+
if (authorization.authorized)
|
|
5303
|
+
return;
|
|
5304
|
+
const reason = callerSeniorReason(authorization);
|
|
5305
|
+
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 });
|
|
5306
|
+
}
|
|
5218
5307
|
function laneGrantTargetHostedHere(ctx, lane) {
|
|
5219
5308
|
if (lane.host.kind === "local") {
|
|
5220
5309
|
return laneRunsHere(lane, reconcileHostFromCtx(ctx));
|
|
@@ -5254,6 +5343,10 @@ async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContex
|
|
|
5254
5343
|
}
|
|
5255
5344
|
await refreshRemoteLaneMirrors(ctx, hqRoot, loadAllLanes(hqRoot), parentLaneIds);
|
|
5256
5345
|
}
|
|
5346
|
+
const remoteSenior = authorization === "senior" &&
|
|
5347
|
+
initial.senior.kind === "lane" && initial.senior.id
|
|
5348
|
+
? await readRemoteHireGrantSenior(ctx, hqRoot, initial.senior.id)
|
|
5349
|
+
: undefined;
|
|
5257
5350
|
const updated = ctx.withGrantHireLocks(hqRoot, lockedLaneIds, () => {
|
|
5258
5351
|
const current = readLane(hqRoot, laneId);
|
|
5259
5352
|
const currentCallerAuthorization = authorization === "senior"
|
|
@@ -5270,11 +5363,18 @@ async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContex
|
|
|
5270
5363
|
(!grantContext || !sameHireGrantSenior(current.senior, grantContext.expected_senior))) {
|
|
5271
5364
|
throw new LanesError("hire_grant_senior_changed", `Lane ${laneId}'s senior changed before the Outpost applied the grant update`, { lane_id: laneId, current_senior: current.senior });
|
|
5272
5365
|
}
|
|
5273
|
-
|
|
5366
|
+
if (remoteSenior !== undefined &&
|
|
5367
|
+
(current.senior.kind !== "lane" || current.senior.id !== remoteSenior.lane_id)) {
|
|
5368
|
+
throw new LanesError("hire_grant_senior_changed", `Lane ${current.lane_id}'s senior changed while its owning Outpost was being checked`, { lane_id: current.lane_id, expected_senior_lane_id: remoteSenior.lane_id, current_senior: current.senior });
|
|
5369
|
+
}
|
|
5370
|
+
const currentHierarchy = grantContext
|
|
5274
5371
|
? authorization === "ssm-owner-dispatch"
|
|
5275
5372
|
? mergeOwnerLaneHierarchy(grantContext.hierarchy, loadAllLanes(hqRoot))
|
|
5276
5373
|
: grantContext.hierarchy
|
|
5277
5374
|
: loadAllLanes(hqRoot);
|
|
5375
|
+
const hierarchy = remoteSenior === undefined
|
|
5376
|
+
? currentHierarchy
|
|
5377
|
+
: hierarchyWithRemoteSenior(currentHierarchy, remoteSenior);
|
|
5278
5378
|
if (current.senior.kind === "lane") {
|
|
5279
5379
|
if (!current.senior.id) {
|
|
5280
5380
|
throw new LanesError("hire_grant_senior_invalid", `Lane ${laneId} has a lane senior without an id`, {
|
|
@@ -6356,84 +6456,6 @@ function laneWithRecoveredProviderSession(hqRoot, lane) {
|
|
|
6356
6456
|
: {}),
|
|
6357
6457
|
};
|
|
6358
6458
|
}
|
|
6359
|
-
function storyCommandTarget(lane, storyId, project) {
|
|
6360
|
-
const projectId = project?.trim() || lane.project_id;
|
|
6361
|
-
const normalizedStoryId = storyId.trim();
|
|
6362
|
-
if (!projectId) {
|
|
6363
|
-
throw new LanesError("missing_project", "--project is required", {});
|
|
6364
|
-
}
|
|
6365
|
-
if (!normalizedStoryId) {
|
|
6366
|
-
throw new LanesError("missing_story", "<story-id> is required", {});
|
|
6367
|
-
}
|
|
6368
|
-
const story = laneStory(lane, projectId, normalizedStoryId);
|
|
6369
|
-
if (!story) {
|
|
6370
|
-
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 });
|
|
6371
|
-
}
|
|
6372
|
-
return { projectId, storyId: normalizedStoryId, story };
|
|
6373
|
-
}
|
|
6374
|
-
function updateCarriedStory(lane, projectId, storyId, status, at, extras = {}) {
|
|
6375
|
-
const stories = laneStories(lane).map((story) => story.project_id === projectId && story.story_id === storyId
|
|
6376
|
-
? {
|
|
6377
|
-
...story,
|
|
6378
|
-
status,
|
|
6379
|
-
updated_at: at,
|
|
6380
|
-
blocked_reason: extras.blocked_reason,
|
|
6381
|
-
verified_at: extras.verified_at,
|
|
6382
|
-
verified_by: extras.verified_by,
|
|
6383
|
-
}
|
|
6384
|
-
: story);
|
|
6385
|
-
return {
|
|
6386
|
-
...lane,
|
|
6387
|
-
project_id: projectId,
|
|
6388
|
-
story_id: storyId,
|
|
6389
|
-
stories,
|
|
6390
|
-
};
|
|
6391
|
-
}
|
|
6392
|
-
function addCarriedStory(lane, projectId, storyId, at) {
|
|
6393
|
-
if (laneStory(lane, projectId, storyId)) {
|
|
6394
|
-
throw new LanesError("story_already_on_lane", `Story ${projectId}/${storyId} is already carried by lane ${lane.lane_id}`, {
|
|
6395
|
-
lane_id: lane.lane_id,
|
|
6396
|
-
project_id: projectId,
|
|
6397
|
-
story_id: storyId,
|
|
6398
|
-
});
|
|
6399
|
-
}
|
|
6400
|
-
return {
|
|
6401
|
-
...lane,
|
|
6402
|
-
...(lane.story_id.trim() ? {} : { project_id: projectId, story_id: storyId }),
|
|
6403
|
-
stories: [
|
|
6404
|
-
...laneStories(lane),
|
|
6405
|
-
{ project_id: projectId, story_id: storyId, status: "in_progress", added_at: at, updated_at: at },
|
|
6406
|
-
],
|
|
6407
|
-
};
|
|
6408
|
-
}
|
|
6409
|
-
function callerMayVerifyStory(ctx, lane) {
|
|
6410
|
-
const callerLaneId = ctx.env.HQ_LANE_ID?.trim();
|
|
6411
|
-
if (callerLaneId) {
|
|
6412
|
-
return lane.senior.kind === "lane" && lane.senior.id === callerLaneId;
|
|
6413
|
-
}
|
|
6414
|
-
if (lane.senior.kind === "user")
|
|
6415
|
-
return true;
|
|
6416
|
-
if (lane.senior.kind === "lane")
|
|
6417
|
-
return false;
|
|
6418
|
-
return sessionIdFromEnv(ctx.env) === lane.senior.id;
|
|
6419
|
-
}
|
|
6420
|
-
async function ensureStoryThread(ctx, lane, projectId, storyId = lane.story_id) {
|
|
6421
|
-
const company = lane.mesh_company ?? lane.company;
|
|
6422
|
-
if (!company)
|
|
6423
|
-
return lane;
|
|
6424
|
-
return ensureLaneThreadRecord(ctx, lane, projectId, storyId);
|
|
6425
|
-
}
|
|
6426
|
-
async function ensureStoryThreadForTransition(ctx, lane, projectId, storyId) {
|
|
6427
|
-
try {
|
|
6428
|
-
return { lane: await ensureStoryThread(ctx, lane, projectId, storyId) };
|
|
6429
|
-
}
|
|
6430
|
-
catch (err) {
|
|
6431
|
-
const warning = storyNotOnBoardWarning(err);
|
|
6432
|
-
if (warning === undefined)
|
|
6433
|
-
throw err;
|
|
6434
|
-
return { lane, warning };
|
|
6435
|
-
}
|
|
6436
|
-
}
|
|
6437
6459
|
async function ensureLaneThreadRecord(ctx, lane, projectId = lane.project_id, storyId = lane.story_id, laneStatus = lane.state) {
|
|
6438
6460
|
const company = lane.mesh_company ?? lane.company;
|
|
6439
6461
|
if (!company)
|
|
@@ -6455,228 +6477,6 @@ async function ensureLaneThreadRecord(ctx, lane, projectId = lane.project_id, st
|
|
|
6455
6477
|
}
|
|
6456
6478
|
return updated;
|
|
6457
6479
|
}
|
|
6458
|
-
async function emitStoryTransition(ctx, lane, projectId, storyId, event, status, payload = {}, skipStoryStatusPatch = false) {
|
|
6459
|
-
const company = lane.mesh_company ?? lane.company;
|
|
6460
|
-
if (!company)
|
|
6461
|
-
return;
|
|
6462
|
-
const ready = await ensureStoryThread(ctx, lane, projectId, storyId);
|
|
6463
|
-
if (!ready.thread_id)
|
|
6464
|
-
return;
|
|
6465
|
-
await emitLaneMeshEvent(ctx, company, ready.thread_id, event, {
|
|
6466
|
-
lane_id: ready.lane_id,
|
|
6467
|
-
project_id: projectId,
|
|
6468
|
-
story_id: storyId,
|
|
6469
|
-
...payload,
|
|
6470
|
-
lane_status: lane.state,
|
|
6471
|
-
});
|
|
6472
|
-
if (!skipStoryStatusPatch && projectId?.trim() && storyId?.trim()) {
|
|
6473
|
-
await ctx.mesh.patchStoryStatus(company, projectId, storyId, status === "blocked" ? "in_progress" : status);
|
|
6474
|
-
}
|
|
6475
|
-
}
|
|
6476
|
-
function sameCommittedStory(current, expected, projectId, storyId) {
|
|
6477
|
-
const currentStory = laneStory(current, projectId, storyId);
|
|
6478
|
-
const expectedStory = laneStory(expected, projectId, storyId);
|
|
6479
|
-
return (current.project_id === expected.project_id &&
|
|
6480
|
-
current.story_id === expected.story_id &&
|
|
6481
|
-
JSON.stringify(currentStory) === JSON.stringify(expectedStory));
|
|
6482
|
-
}
|
|
6483
|
-
async function commitStoryTransition(ctx, before, next, projectId, storyId, event, status, payload = {}, skipStoryStatusPatch = false) {
|
|
6484
|
-
try {
|
|
6485
|
-
await emitStoryTransition(ctx, next, projectId, storyId, event, status, payload, skipStoryStatusPatch);
|
|
6486
|
-
return undefined;
|
|
6487
|
-
}
|
|
6488
|
-
catch (err) {
|
|
6489
|
-
const warning = storyNotOnBoardWarning(err);
|
|
6490
|
-
if (warning !== undefined)
|
|
6491
|
-
return warning;
|
|
6492
|
-
try {
|
|
6493
|
-
const current = readLane(ctx.hqRoot(), next.lane_id);
|
|
6494
|
-
if (!sameCommittedStory(current, next, projectId, storyId)) {
|
|
6495
|
-
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 });
|
|
6496
|
-
}
|
|
6497
|
-
updateLane(ctx.hqRoot(), next.lane_id, current.version, "command", (latest) => {
|
|
6498
|
-
if (!sameCommittedStory(latest, next, projectId, storyId)) {
|
|
6499
|
-
throw new LanesError("story_transition_rollback_conflict", `Story ${projectId}/${storyId} changed before rollback`, { lane_id: next.lane_id, project_id: projectId, story_id: storyId });
|
|
6500
|
-
}
|
|
6501
|
-
return {
|
|
6502
|
-
...latest,
|
|
6503
|
-
project_id: before.project_id,
|
|
6504
|
-
story_id: before.story_id,
|
|
6505
|
-
stories: before.stories,
|
|
6506
|
-
};
|
|
6507
|
-
});
|
|
6508
|
-
}
|
|
6509
|
-
catch (rollbackErr) {
|
|
6510
|
-
process.stderr.write(`hq lanes: story transition rollback failed for ${next.lane_id}/${projectId}/${storyId}: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}\n`);
|
|
6511
|
-
}
|
|
6512
|
-
throw err;
|
|
6513
|
-
}
|
|
6514
|
-
}
|
|
6515
|
-
async function runStoryList(ctx, laneId) {
|
|
6516
|
-
const lane = readLane(ctx.hqRoot(), laneId);
|
|
6517
|
-
printJson({
|
|
6518
|
-
ok: true,
|
|
6519
|
-
action: "story.list",
|
|
6520
|
-
lane_id: laneId,
|
|
6521
|
-
stories: laneStories(lane),
|
|
6522
|
-
active_stories: activeLaneStories(lane),
|
|
6523
|
-
story_warning: storyWarning(lane) ?? null,
|
|
6524
|
-
});
|
|
6525
|
-
}
|
|
6526
|
-
async function runStoryAdd(ctx, laneId, storyId, opts) {
|
|
6527
|
-
const hqRoot = ctx.hqRoot();
|
|
6528
|
-
let lane = readLane(hqRoot, laneId);
|
|
6529
|
-
assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
|
|
6530
|
-
const projectId = opts.project?.trim() || lane.project_id;
|
|
6531
|
-
if (!projectId)
|
|
6532
|
-
throw new LanesError("missing_project", "--project is required", {});
|
|
6533
|
-
const normalizedStoryId = storyId.trim();
|
|
6534
|
-
if (!normalizedStoryId)
|
|
6535
|
-
throw new LanesError("missing_story", "<story-id> is required", {});
|
|
6536
|
-
if (ctx.storyLedgerEnabled()) {
|
|
6537
|
-
validateProjectStories(hqRoot, projectId, [normalizedStoryId], lane.company?.slug);
|
|
6538
|
-
}
|
|
6539
|
-
const company = lane.mesh_company ?? lane.company;
|
|
6540
|
-
const warnings = [];
|
|
6541
|
-
let storyKnownAbsent = false;
|
|
6542
|
-
if (company) {
|
|
6543
|
-
const boardCheck = await storyBoardCheck(ctx, company, projectId, [normalizedStoryId]);
|
|
6544
|
-
if (boardCheck.warning !== undefined)
|
|
6545
|
-
warnings.push(boardCheck.warning);
|
|
6546
|
-
storyKnownAbsent = boardCheck.absentStoryIds.has(normalizedStoryId);
|
|
6547
|
-
}
|
|
6548
|
-
const at = ctx.now().toISOString();
|
|
6549
|
-
const thread = await ensureStoryThreadForTransition(ctx, lane, projectId, normalizedStoryId);
|
|
6550
|
-
lane = thread.lane;
|
|
6551
|
-
if (thread.warning !== undefined)
|
|
6552
|
-
warnings.push(thread.warning);
|
|
6553
|
-
const before = lane;
|
|
6554
|
-
const next = casUpdateLane(hqRoot, laneId, (current) => {
|
|
6555
|
-
assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
|
|
6556
|
-
const added = addCarriedStory(current, projectId, normalizedStoryId, at);
|
|
6557
|
-
return {
|
|
6558
|
-
...added,
|
|
6559
|
-
no_story_reason: undefined,
|
|
6560
|
-
};
|
|
6561
|
-
});
|
|
6562
|
-
const transitionWarning = await commitStoryTransition(ctx, before, next, projectId, normalizedStoryId, "lane_started", "in_progress", {}, storyKnownAbsent);
|
|
6563
|
-
if (transitionWarning !== undefined)
|
|
6564
|
-
warnings.push(transitionWarning);
|
|
6565
|
-
const finalLane = readLane(hqRoot, laneId);
|
|
6566
|
-
printJson({
|
|
6567
|
-
ok: true,
|
|
6568
|
-
action: "story.add",
|
|
6569
|
-
lane_id: laneId,
|
|
6570
|
-
project_id: projectId,
|
|
6571
|
-
story_id: normalizedStoryId,
|
|
6572
|
-
status: "in_progress",
|
|
6573
|
-
story_warning: combinedStoryWarning(finalLane, warnings),
|
|
6574
|
-
lane: finalLane,
|
|
6575
|
-
});
|
|
6576
|
-
}
|
|
6577
|
-
async function runStoryStart(ctx, laneId, storyId, opts) {
|
|
6578
|
-
const hqRoot = ctx.hqRoot();
|
|
6579
|
-
let lane = readLane(hqRoot, laneId);
|
|
6580
|
-
assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
|
|
6581
|
-
const target = storyCommandTarget(lane, storyId, opts.project);
|
|
6582
|
-
if (target.story.status === "done") {
|
|
6583
|
-
throw new LanesError("story_already_done", `Story ${target.projectId}/${target.storyId} is done and cannot be started again`, {
|
|
6584
|
-
lane_id: laneId,
|
|
6585
|
-
project_id: target.projectId,
|
|
6586
|
-
story_id: target.storyId,
|
|
6587
|
-
});
|
|
6588
|
-
}
|
|
6589
|
-
const at = ctx.now().toISOString();
|
|
6590
|
-
const warnings = [];
|
|
6591
|
-
const thread = await ensureStoryThreadForTransition(ctx, lane, target.projectId, target.storyId);
|
|
6592
|
-
lane = thread.lane;
|
|
6593
|
-
if (thread.warning !== undefined)
|
|
6594
|
-
warnings.push(thread.warning);
|
|
6595
|
-
const before = lane;
|
|
6596
|
-
const next = casUpdateLane(hqRoot, laneId, (current) => {
|
|
6597
|
-
assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
|
|
6598
|
-
return updateCarriedStory(current, target.projectId, target.storyId, "in_progress", at);
|
|
6599
|
-
});
|
|
6600
|
-
const transitionWarning = await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_started", "in_progress");
|
|
6601
|
-
if (transitionWarning !== undefined)
|
|
6602
|
-
warnings.push(transitionWarning);
|
|
6603
|
-
const finalLane = readLane(hqRoot, laneId);
|
|
6604
|
-
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 });
|
|
6605
|
-
}
|
|
6606
|
-
async function runStoryBlock(ctx, laneId, storyId, opts) {
|
|
6607
|
-
const hqRoot = ctx.hqRoot();
|
|
6608
|
-
let lane = readLane(hqRoot, laneId);
|
|
6609
|
-
assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
|
|
6610
|
-
const target = storyCommandTarget(lane, storyId, opts.project);
|
|
6611
|
-
if (target.story.status === "done") {
|
|
6612
|
-
throw new LanesError("story_already_done", `Story ${target.projectId}/${target.storyId} is done and cannot be blocked again`, {
|
|
6613
|
-
lane_id: laneId,
|
|
6614
|
-
project_id: target.projectId,
|
|
6615
|
-
story_id: target.storyId,
|
|
6616
|
-
});
|
|
6617
|
-
}
|
|
6618
|
-
const reason = opts.reason?.trim();
|
|
6619
|
-
if (!reason)
|
|
6620
|
-
throw new LanesError("missing_block_reason", "--reason is required", {});
|
|
6621
|
-
const at = ctx.now().toISOString();
|
|
6622
|
-
const warnings = [];
|
|
6623
|
-
const thread = await ensureStoryThreadForTransition(ctx, lane, target.projectId, target.storyId);
|
|
6624
|
-
lane = thread.lane;
|
|
6625
|
-
if (thread.warning !== undefined)
|
|
6626
|
-
warnings.push(thread.warning);
|
|
6627
|
-
const before = lane;
|
|
6628
|
-
const next = casUpdateLane(hqRoot, laneId, (current) => {
|
|
6629
|
-
assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
|
|
6630
|
-
return updateCarriedStory(current, target.projectId, target.storyId, "blocked", at, { blocked_reason: reason });
|
|
6631
|
-
});
|
|
6632
|
-
const transitionWarning = await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_blocked", "blocked", { blocked_reason: reason });
|
|
6633
|
-
if (transitionWarning !== undefined)
|
|
6634
|
-
warnings.push(transitionWarning);
|
|
6635
|
-
const finalLane = readLane(hqRoot, laneId);
|
|
6636
|
-
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 });
|
|
6637
|
-
}
|
|
6638
|
-
async function runStoryDone(ctx, laneId, storyId, opts) {
|
|
6639
|
-
const hqRoot = ctx.hqRoot();
|
|
6640
|
-
let lane = readLane(hqRoot, laneId);
|
|
6641
|
-
assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
|
|
6642
|
-
if (!opts.verified) {
|
|
6643
|
-
throw new LanesError("senior_verification_required", "Only a senior may mark a story done; pass --verified after reviewing or merging it", { lane_id: laneId });
|
|
6644
|
-
}
|
|
6645
|
-
if (!callerMayVerifyStory(ctx, lane)) {
|
|
6646
|
-
throw new LanesError("senior_verification_required", `Caller is not the senior for lane ${laneId}`, {
|
|
6647
|
-
lane_id: laneId,
|
|
6648
|
-
senior: lane.senior,
|
|
6649
|
-
});
|
|
6650
|
-
}
|
|
6651
|
-
const target = storyCommandTarget(lane, storyId, opts.project);
|
|
6652
|
-
if (target.story.status !== "review") {
|
|
6653
|
-
throw new LanesError("story_not_ready_for_verification", `Story ${target.projectId}/${target.storyId} must be in review before senior verification`, {
|
|
6654
|
-
lane_id: laneId,
|
|
6655
|
-
project_id: target.projectId,
|
|
6656
|
-
story_id: target.storyId,
|
|
6657
|
-
status: target.story.status,
|
|
6658
|
-
});
|
|
6659
|
-
}
|
|
6660
|
-
const at = ctx.now().toISOString();
|
|
6661
|
-
const warnings = [];
|
|
6662
|
-
const thread = await ensureStoryThreadForTransition(ctx, lane, target.projectId, target.storyId);
|
|
6663
|
-
lane = thread.lane;
|
|
6664
|
-
if (thread.warning !== undefined)
|
|
6665
|
-
warnings.push(thread.warning);
|
|
6666
|
-
const before = lane;
|
|
6667
|
-
const next = casUpdateLane(hqRoot, laneId, (current) => {
|
|
6668
|
-
assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
|
|
6669
|
-
return updateCarriedStory(current, target.projectId, target.storyId, "done", at, {
|
|
6670
|
-
verified_at: at,
|
|
6671
|
-
verified_by: current.senior,
|
|
6672
|
-
});
|
|
6673
|
-
});
|
|
6674
|
-
const transitionWarning = await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_done", "done", { summary: "Senior verification complete" });
|
|
6675
|
-
if (transitionWarning !== undefined)
|
|
6676
|
-
warnings.push(transitionWarning);
|
|
6677
|
-
const finalLane = readLane(hqRoot, laneId);
|
|
6678
|
-
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 });
|
|
6679
|
-
}
|
|
6680
6480
|
/**
|
|
6681
6481
|
* Registration state of the drain hook in the lane's account home, read the
|
|
6682
6482
|
* way the provider reads it. Null when the lane has no bound account yet
|
|
@@ -7443,7 +7243,23 @@ export function interruptNotResumableWarning(laneId) {
|
|
|
7443
7243
|
return `hq lanes: warning: lane ${laneId} was interrupted before its provider reported a session id, ` +
|
|
7444
7244
|
`so hq lanes resume cannot continue it. Start a new lane for the remaining work.\n`;
|
|
7445
7245
|
}
|
|
7246
|
+
function parseQuestionPriority(value) {
|
|
7247
|
+
if (value === undefined) {
|
|
7248
|
+
if (ASK_PRIORITY_REQUIRED) {
|
|
7249
|
+
throw new LanesError("ask_priority_required", "--priority is required; choose P0, P1, or P2", {
|
|
7250
|
+
allowed: [...QUESTION_PRIORITIES],
|
|
7251
|
+
});
|
|
7252
|
+
}
|
|
7253
|
+
return undefined;
|
|
7254
|
+
}
|
|
7255
|
+
const normalized = value.toUpperCase();
|
|
7256
|
+
if (!QUESTION_PRIORITIES.includes(normalized)) {
|
|
7257
|
+
throw new LanesError("ask_priority_invalid", `Invalid --priority ${JSON.stringify(value)}. Allowed values: P0, P1, P2`, { allowed: [...QUESTION_PRIORITIES], value });
|
|
7258
|
+
}
|
|
7259
|
+
return normalized;
|
|
7260
|
+
}
|
|
7446
7261
|
async function runAsk(ctx, opts) {
|
|
7262
|
+
const priority = parseQuestionPriority(opts.priority);
|
|
7447
7263
|
const hqRoot = ctx.hqRoot();
|
|
7448
7264
|
const laneId = ctx.env.HQ_LANE_ID?.trim();
|
|
7449
7265
|
if (!laneId) {
|
|
@@ -7482,6 +7298,7 @@ async function runAsk(ctx, opts) {
|
|
|
7482
7298
|
at,
|
|
7483
7299
|
activeCompany: questionCompany,
|
|
7484
7300
|
questionId,
|
|
7301
|
+
priority,
|
|
7485
7302
|
});
|
|
7486
7303
|
questionId = withBody.question_id;
|
|
7487
7304
|
await emitLaneMeshEvent(ctx, lane.mesh_company ?? lane.company, lane.thread_id, "lane_question", {
|
|
@@ -7496,12 +7313,16 @@ async function runAsk(ctx, opts) {
|
|
|
7496
7313
|
question_id: withBody.question_id,
|
|
7497
7314
|
});
|
|
7498
7315
|
}
|
|
7316
|
+
if (priority === undefined) {
|
|
7317
|
+
process.stderr.write(`hq lanes ask: warning: ${ASK_PRIORITY_WARNING}\n`);
|
|
7318
|
+
}
|
|
7499
7319
|
printJson({
|
|
7500
7320
|
ok: true,
|
|
7501
7321
|
action: "ask",
|
|
7502
7322
|
question: withBody,
|
|
7503
7323
|
lane_id: lane.lane_id,
|
|
7504
7324
|
lane_state: lane.state,
|
|
7325
|
+
...(priority === undefined ? { warnings: [ASK_PRIORITY_WARNING] } : {}),
|
|
7505
7326
|
});
|
|
7506
7327
|
return;
|
|
7507
7328
|
}
|
|
@@ -7940,6 +7761,19 @@ function attachJsonNoop(cmd) {
|
|
|
7940
7761
|
}
|
|
7941
7762
|
export function registerLanesCommand(parent, deps = {}) {
|
|
7942
7763
|
const ctx = resolveDeps(deps);
|
|
7764
|
+
const storyHandlers = createLaneStoryHandlers({
|
|
7765
|
+
assertLaneRunsHereForOperatorMutation,
|
|
7766
|
+
casUpdateLane,
|
|
7767
|
+
combinedStoryWarning,
|
|
7768
|
+
emitLaneMeshEvent,
|
|
7769
|
+
ensureLaneThreadRecord,
|
|
7770
|
+
printJson,
|
|
7771
|
+
readLane,
|
|
7772
|
+
storyBoardCheck,
|
|
7773
|
+
storyNotOnBoardWarning,
|
|
7774
|
+
updateLane,
|
|
7775
|
+
validateProjectStories,
|
|
7776
|
+
});
|
|
7943
7777
|
const accountHandlers = createLaneAccountsHandlers({
|
|
7944
7778
|
automaticBalanceMoveHistoryReason,
|
|
7945
7779
|
ensureDir,
|
|
@@ -7957,6 +7791,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
7957
7791
|
MAX_PLACEMENT_OUTPOSTS,
|
|
7958
7792
|
OUTPOST_QUESTION_LOOKUP_CONCURRENCY,
|
|
7959
7793
|
assertLaneRunsHereForOperatorMutation,
|
|
7794
|
+
assertQuestionAnswerAuthorized,
|
|
7960
7795
|
assertQuestionCompanyMatchesLane,
|
|
7961
7796
|
casUpdateQuestion,
|
|
7962
7797
|
effectiveLaneQuestionCompany,
|
|
@@ -8095,6 +7930,11 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8095
7930
|
}, wrap);
|
|
8096
7931
|
registerOutpostPrunerCommand(lanes);
|
|
8097
7932
|
registerLaneKpisCommand(lanes, { hqRoot: ctx.hqRoot, now: ctx.now, wrap });
|
|
7933
|
+
registerLaneAutopilotReportCommand(lanes, {
|
|
7934
|
+
hqRoot: ctx.hqRoot,
|
|
7935
|
+
now: ctx.now,
|
|
7936
|
+
currentHost: () => resolveAdmissionHost({ hostname: ctx.hostname, readOutpostIdentity: ctx.readOutpostIdentity }),
|
|
7937
|
+
}, wrap);
|
|
8098
7938
|
const noStoryOption = new Option("--no-story <reason>", "Create a lane without a story and record why");
|
|
8099
7939
|
noStoryOption.attributeName = () => "noStory";
|
|
8100
7940
|
lanes
|
|
@@ -8306,7 +8146,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8306
8146
|
.description("List the stories carried by a lane")
|
|
8307
8147
|
.argument("<lane-id>", "Lane id")
|
|
8308
8148
|
.action(wrap(async (laneId) => {
|
|
8309
|
-
await runStoryList(ctx, laneId);
|
|
8149
|
+
await storyHandlers.runStoryList(ctx, laneId);
|
|
8310
8150
|
}));
|
|
8311
8151
|
stories
|
|
8312
8152
|
.command("add")
|
|
@@ -8315,7 +8155,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8315
8155
|
.argument("<story-id>", "Story id")
|
|
8316
8156
|
.option("--project <slug>", "Project id; defaults to the lane's current project")
|
|
8317
8157
|
.action(wrap(async (laneId, storyId, opts) => {
|
|
8318
|
-
await runStoryAdd(ctx, laneId, storyId, opts);
|
|
8158
|
+
await storyHandlers.runStoryAdd(ctx, laneId, storyId, opts);
|
|
8319
8159
|
}));
|
|
8320
8160
|
stories
|
|
8321
8161
|
.command("start")
|
|
@@ -8324,7 +8164,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8324
8164
|
.argument("<story-id>", "Story id")
|
|
8325
8165
|
.option("--project <slug>", "Project id; defaults to the lane's current project")
|
|
8326
8166
|
.action(wrap(async (laneId, storyId, opts) => {
|
|
8327
|
-
await runStoryStart(ctx, laneId, storyId, opts);
|
|
8167
|
+
await storyHandlers.runStoryStart(ctx, laneId, storyId, opts);
|
|
8328
8168
|
}));
|
|
8329
8169
|
stories
|
|
8330
8170
|
.command("block")
|
|
@@ -8334,7 +8174,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8334
8174
|
.requiredOption("--reason <text>", "Why the story is blocked")
|
|
8335
8175
|
.option("--project <slug>", "Project id; defaults to the lane's current project")
|
|
8336
8176
|
.action(wrap(async (laneId, storyId, opts) => {
|
|
8337
|
-
await runStoryBlock(ctx, laneId, storyId, opts);
|
|
8177
|
+
await storyHandlers.runStoryBlock(ctx, laneId, storyId, opts);
|
|
8338
8178
|
}));
|
|
8339
8179
|
stories
|
|
8340
8180
|
.command("done")
|
|
@@ -8344,7 +8184,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8344
8184
|
.requiredOption("--verified", "Confirm senior review or merge")
|
|
8345
8185
|
.option("--project <slug>", "Project id; defaults to the lane's current project")
|
|
8346
8186
|
.action(wrap(async (laneId, storyId, opts) => {
|
|
8347
|
-
await runStoryDone(ctx, laneId, storyId, opts);
|
|
8187
|
+
await storyHandlers.runStoryDone(ctx, laneId, storyId, opts);
|
|
8348
8188
|
}));
|
|
8349
8189
|
lanes
|
|
8350
8190
|
.command("message")
|
|
@@ -8493,6 +8333,8 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8493
8333
|
.option("--context <text>", "Context for the audience")
|
|
8494
8334
|
.option("--option <text>", "Answer choice (repeatable)", collectRepeatable)
|
|
8495
8335
|
.option("--recommended <text>", "Recommended option")
|
|
8336
|
+
.option("--priority <P0|P1|P2>", "Urgency: P0 blocks now, P1 needed today, P2 can wait")
|
|
8337
|
+
.addHelpText("after", "\nThe audience may take up to 24 hours to answer; keep working meanwhile.\n")
|
|
8496
8338
|
.action(wrap(async (opts) => {
|
|
8497
8339
|
await runAsk(ctx, opts);
|
|
8498
8340
|
}));
|
|
@@ -8631,9 +8473,11 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8631
8473
|
.action(wrap(async (opts) => {
|
|
8632
8474
|
await runPickup(ctx, opts);
|
|
8633
8475
|
}));
|
|
8634
|
-
lanes
|
|
8635
|
-
|
|
8636
|
-
|
|
8476
|
+
const autopilot = lanes.commands.find((command) => command.name() === "autopilot");
|
|
8477
|
+
if (!autopilot)
|
|
8478
|
+
throw new Error("Lanes autopilot command group was not registered");
|
|
8479
|
+
autopilot
|
|
8480
|
+
.description("Run lead autopilot checks and report follow-through metrics")
|
|
8637
8481
|
.command("run")
|
|
8638
8482
|
.description("Run one lead autopilot check pass")
|
|
8639
8483
|
.option("--dry-run", "Report proposed wakes without writing them")
|