@indigoai-us/hq-cli 5.293.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 +14 -0
- package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +5 -1
- package/dist/command-catalog.generated.d.ts +42 -24
- package/dist/command-catalog.generated.js +53 -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-questions.js +8 -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.js +39 -309
- 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/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/lanes/autopilot-metrics.d.ts +99 -0
- package/dist/lib/lanes/autopilot-metrics.js +301 -0
- package/dist/lib/lanes/store.d.ts +2 -0
- package/dist/lib/lanes/store.js +32 -3
- 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/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";
|
|
@@ -5295,6 +5297,13 @@ function appendCallerSeniorReason(message, authorization) {
|
|
|
5295
5297
|
? message
|
|
5296
5298
|
: `${message}; authorization refused: ${callerSeniorReason(authorization)}`;
|
|
5297
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
|
+
}
|
|
5298
5307
|
function laneGrantTargetHostedHere(ctx, lane) {
|
|
5299
5308
|
if (lane.host.kind === "local") {
|
|
5300
5309
|
return laneRunsHere(lane, reconcileHostFromCtx(ctx));
|
|
@@ -6447,84 +6456,6 @@ function laneWithRecoveredProviderSession(hqRoot, lane) {
|
|
|
6447
6456
|
: {}),
|
|
6448
6457
|
};
|
|
6449
6458
|
}
|
|
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
6459
|
async function ensureLaneThreadRecord(ctx, lane, projectId = lane.project_id, storyId = lane.story_id, laneStatus = lane.state) {
|
|
6529
6460
|
const company = lane.mesh_company ?? lane.company;
|
|
6530
6461
|
if (!company)
|
|
@@ -6546,228 +6477,6 @@ async function ensureLaneThreadRecord(ctx, lane, projectId = lane.project_id, st
|
|
|
6546
6477
|
}
|
|
6547
6478
|
return updated;
|
|
6548
6479
|
}
|
|
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
6480
|
/**
|
|
6772
6481
|
* Registration state of the drain hook in the lane's account home, read the
|
|
6773
6482
|
* way the provider reads it. Null when the lane has no bound account yet
|
|
@@ -8052,6 +7761,19 @@ function attachJsonNoop(cmd) {
|
|
|
8052
7761
|
}
|
|
8053
7762
|
export function registerLanesCommand(parent, deps = {}) {
|
|
8054
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
|
+
});
|
|
8055
7777
|
const accountHandlers = createLaneAccountsHandlers({
|
|
8056
7778
|
automaticBalanceMoveHistoryReason,
|
|
8057
7779
|
ensureDir,
|
|
@@ -8069,6 +7791,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8069
7791
|
MAX_PLACEMENT_OUTPOSTS,
|
|
8070
7792
|
OUTPOST_QUESTION_LOOKUP_CONCURRENCY,
|
|
8071
7793
|
assertLaneRunsHereForOperatorMutation,
|
|
7794
|
+
assertQuestionAnswerAuthorized,
|
|
8072
7795
|
assertQuestionCompanyMatchesLane,
|
|
8073
7796
|
casUpdateQuestion,
|
|
8074
7797
|
effectiveLaneQuestionCompany,
|
|
@@ -8207,6 +7930,11 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8207
7930
|
}, wrap);
|
|
8208
7931
|
registerOutpostPrunerCommand(lanes);
|
|
8209
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);
|
|
8210
7938
|
const noStoryOption = new Option("--no-story <reason>", "Create a lane without a story and record why");
|
|
8211
7939
|
noStoryOption.attributeName = () => "noStory";
|
|
8212
7940
|
lanes
|
|
@@ -8418,7 +8146,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8418
8146
|
.description("List the stories carried by a lane")
|
|
8419
8147
|
.argument("<lane-id>", "Lane id")
|
|
8420
8148
|
.action(wrap(async (laneId) => {
|
|
8421
|
-
await runStoryList(ctx, laneId);
|
|
8149
|
+
await storyHandlers.runStoryList(ctx, laneId);
|
|
8422
8150
|
}));
|
|
8423
8151
|
stories
|
|
8424
8152
|
.command("add")
|
|
@@ -8427,7 +8155,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8427
8155
|
.argument("<story-id>", "Story id")
|
|
8428
8156
|
.option("--project <slug>", "Project id; defaults to the lane's current project")
|
|
8429
8157
|
.action(wrap(async (laneId, storyId, opts) => {
|
|
8430
|
-
await runStoryAdd(ctx, laneId, storyId, opts);
|
|
8158
|
+
await storyHandlers.runStoryAdd(ctx, laneId, storyId, opts);
|
|
8431
8159
|
}));
|
|
8432
8160
|
stories
|
|
8433
8161
|
.command("start")
|
|
@@ -8436,7 +8164,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8436
8164
|
.argument("<story-id>", "Story id")
|
|
8437
8165
|
.option("--project <slug>", "Project id; defaults to the lane's current project")
|
|
8438
8166
|
.action(wrap(async (laneId, storyId, opts) => {
|
|
8439
|
-
await runStoryStart(ctx, laneId, storyId, opts);
|
|
8167
|
+
await storyHandlers.runStoryStart(ctx, laneId, storyId, opts);
|
|
8440
8168
|
}));
|
|
8441
8169
|
stories
|
|
8442
8170
|
.command("block")
|
|
@@ -8446,7 +8174,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8446
8174
|
.requiredOption("--reason <text>", "Why the story is blocked")
|
|
8447
8175
|
.option("--project <slug>", "Project id; defaults to the lane's current project")
|
|
8448
8176
|
.action(wrap(async (laneId, storyId, opts) => {
|
|
8449
|
-
await runStoryBlock(ctx, laneId, storyId, opts);
|
|
8177
|
+
await storyHandlers.runStoryBlock(ctx, laneId, storyId, opts);
|
|
8450
8178
|
}));
|
|
8451
8179
|
stories
|
|
8452
8180
|
.command("done")
|
|
@@ -8456,7 +8184,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8456
8184
|
.requiredOption("--verified", "Confirm senior review or merge")
|
|
8457
8185
|
.option("--project <slug>", "Project id; defaults to the lane's current project")
|
|
8458
8186
|
.action(wrap(async (laneId, storyId, opts) => {
|
|
8459
|
-
await runStoryDone(ctx, laneId, storyId, opts);
|
|
8187
|
+
await storyHandlers.runStoryDone(ctx, laneId, storyId, opts);
|
|
8460
8188
|
}));
|
|
8461
8189
|
lanes
|
|
8462
8190
|
.command("message")
|
|
@@ -8745,9 +8473,11 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8745
8473
|
.action(wrap(async (opts) => {
|
|
8746
8474
|
await runPickup(ctx, opts);
|
|
8747
8475
|
}));
|
|
8748
|
-
lanes
|
|
8749
|
-
|
|
8750
|
-
|
|
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")
|
|
8751
8481
|
.command("run")
|
|
8752
8482
|
.description("Run one lead autopilot check pass")
|
|
8753
8483
|
.option("--dry-run", "Report proposed wakes without writing them")
|
package/dist/fast-core.js
CHANGED
|
@@ -18,43 +18,71 @@ export async function runFastCore(argv) {
|
|
|
18
18
|
const parsed = parseFastCore(argv);
|
|
19
19
|
if (!parsed)
|
|
20
20
|
return;
|
|
21
|
-
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
const entry = parsed.entry;
|
|
22
|
+
// The full graph's process gate accepts textual true/false overrides. It ignores
|
|
23
|
+
// numeric env values, so preserve that behavior for family flags here too.
|
|
24
|
+
const familyEnabled = entry.kind !== "native" || !entry.familyFlagKey ||
|
|
25
|
+
await (await import("./native-fast-route.js")).fastFlagEnabled(entry.familyFlagKey, process.env, { numericEnvironmentOverride: false });
|
|
26
|
+
const useShell = entry.kind === "native" && Boolean(entry.familyFlagKey) && !familyEnabled;
|
|
27
|
+
const shouldInjectRoot = useShell
|
|
28
|
+
? entry.shellHqRoot === "live" || (entry.shellHqRoot === "reconcile-default" &&
|
|
29
|
+
!parsed.args.includes("--hq-root") && !parsed.args.includes("--help") && !parsed.args.includes("-h"))
|
|
30
|
+
: entry.kind === "shell"
|
|
31
|
+
? true
|
|
32
|
+
: entry.root === "live";
|
|
33
|
+
// `root: "live"` — resolve the live HQ root using the same resolver as the
|
|
34
|
+
// full command graph. Existing shell fast entries keep their historical
|
|
35
|
+
// fail-soft behavior; newly routed commands fall back to the full graph so
|
|
36
|
+
// its expected-user-error formatting remains authoritative.
|
|
26
37
|
let hqRoot;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
if (shouldInjectRoot) {
|
|
39
|
+
try {
|
|
40
|
+
hqRoot = resolveLiveRoot(parsed.hqRootArg ? { hqRoot: parsed.hqRootArg } : {});
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
if (entry.kind === "native") {
|
|
44
|
+
const { runCli } = await import("./main.js");
|
|
45
|
+
await runCli();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
hqRoot = undefined;
|
|
49
|
+
}
|
|
32
50
|
}
|
|
33
|
-
if (
|
|
34
|
-
const
|
|
35
|
-
|
|
51
|
+
if (entry.kind === "native" && !useShell) {
|
|
52
|
+
const { installFastPathStreamErrorHandlers } = await import("./utils/fast-stream-errors.js");
|
|
53
|
+
installFastPathStreamErrorHandlers();
|
|
54
|
+
const handlerModule = await import(entry.modulePath);
|
|
55
|
+
const handler = handlerModule[entry.exportName];
|
|
36
56
|
if (typeof handler !== "function") {
|
|
37
|
-
throw new Error(`Native fast handler ${
|
|
57
|
+
throw new Error(`Native fast handler ${entry.exportName} was not exported`);
|
|
38
58
|
}
|
|
39
59
|
const chunks = [];
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
60
|
+
if (entry.readsStdin) {
|
|
61
|
+
for await (const chunk of process.stdin)
|
|
62
|
+
chunks.push(chunk);
|
|
63
|
+
}
|
|
64
|
+
const code = await handler(parsed.args, entry.readsStdin ? Buffer.concat(chunks).toString("utf8") : "", process.cwd(), hqRoot, entry.name.split(" ").at(-1) ?? entry.name, parsed.hqRootArg);
|
|
43
65
|
// Let Node drain handler output before terminating. The timeout guard's
|
|
44
66
|
// stderr message must survive the native route's process boundary.
|
|
45
67
|
process.exitCode = code;
|
|
46
68
|
return;
|
|
47
69
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
//
|
|
70
|
+
const asset = entry.asset;
|
|
71
|
+
if (!asset)
|
|
72
|
+
throw new Error(`Fast route ${entry.name} has no shell companion`);
|
|
73
|
+
// Some historical registrations run from the live root, others preserve the
|
|
74
|
+
// caller cwd and only inject HQ_ROOT. Keep those choices explicit per entry.
|
|
75
|
+
const cwd = entry.kind === "native" && entry.shellCwd === "live"
|
|
76
|
+
? hqRoot ?? process.cwd()
|
|
77
|
+
: process.cwd();
|
|
78
|
+
const injectRoot = entry.kind === "shell" || entry.kind === "native" &&
|
|
79
|
+
(entry.shellHqRoot === "live" || entry.shellHqRoot === "reconcile-default" && shouldInjectRoot);
|
|
52
80
|
const { code } = runBundledScript({
|
|
53
|
-
asset
|
|
81
|
+
asset,
|
|
54
82
|
args: parsed.args,
|
|
55
|
-
cwd
|
|
56
|
-
hqRoot,
|
|
57
|
-
env:
|
|
83
|
+
cwd,
|
|
84
|
+
...(injectRoot ? { hqRoot } : {}),
|
|
85
|
+
env: entry.name === "hq-session" ? { HQ_HQ_SESSION_NO_CLI: "1" } : undefined,
|
|
58
86
|
});
|
|
59
87
|
// Propagate verbatim: these scripts use their exit code as their interface.
|
|
60
88
|
process.exit(code);
|
package/dist/fast-scaffold.d.ts
CHANGED
|
@@ -15,16 +15,29 @@ export type FastScaffoldScript = {
|
|
|
15
15
|
kind: "shell";
|
|
16
16
|
/** Path under `assets/scaffold/`, e.g. `core/scripts/foo.sh`. */
|
|
17
17
|
asset: string;
|
|
18
|
+
root?: "live";
|
|
18
19
|
} | {
|
|
19
|
-
/**
|
|
20
|
+
/** Command path, joined with spaces, e.g. `policy frontmatter`. */
|
|
20
21
|
name: string;
|
|
21
22
|
kind: "native";
|
|
22
23
|
/** Relative ESM module specifier, resolved from fast-core.js. */
|
|
23
24
|
modulePath: string;
|
|
24
25
|
/** Named export which accepts the native fast-handler contract. */
|
|
25
26
|
exportName: string;
|
|
26
|
-
/**
|
|
27
|
+
/** Existing default-off gate for the whole fast route. */
|
|
27
28
|
flagKey: string;
|
|
29
|
+
/** Existing family gate that chooses this native handler or its shell companion. */
|
|
30
|
+
familyFlagKey?: string;
|
|
31
|
+
/** Shell rollback companion used while the family gate is off. */
|
|
32
|
+
asset?: string;
|
|
33
|
+
/** Root context used by the native handler. */
|
|
34
|
+
root?: "live" | "cwd" | "passthrough";
|
|
35
|
+
/** Cwd used for a shell rollback companion. Defaults to the caller cwd. */
|
|
36
|
+
shellCwd?: "caller" | "live";
|
|
37
|
+
/** Root injection behavior for a shell rollback companion. */
|
|
38
|
+
shellHqRoot?: "none" | "live" | "reconcile-default";
|
|
39
|
+
/** Whether this handler consumes stdin. Shell companions inherit stdin directly. */
|
|
40
|
+
readsStdin?: boolean;
|
|
28
41
|
};
|
|
29
42
|
/** The relocated scripts that qualify for the entrypoint fast path. */
|
|
30
43
|
export declare const FAST_SCAFFOLD_SCRIPTS: readonly FastScaffoldScript[];
|