@indigoai-us/hq-cli 5.166.0 → 5.168.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/dist/command-catalog.generated.d.ts +88 -1
- package/dist/command-catalog.generated.js +112 -1
- package/dist/commands/daemon.d.ts +2 -0
- package/dist/commands/daemon.js +79 -22
- package/dist/commands/lanes.js +269 -28
- package/dist/commands/mesh.js +13 -19
- package/dist/commands/outpost-pruner.d.ts +3 -0
- package/dist/commands/outpost-pruner.js +125 -0
- package/dist/lib/daemon/catalog.d.ts +11 -1
- package/dist/lib/daemon/catalog.js +31 -6
- package/dist/lib/daemon/install.d.ts +7 -0
- package/dist/lib/daemon/install.js +18 -2
- package/dist/lib/daemon/legacy.js +6 -1
- package/dist/lib/daemon/machine-type.d.ts +4 -1
- package/dist/lib/daemon/machine-type.js +29 -9
- package/dist/lib/daemon/migrate.js +2 -2
- package/dist/lib/daemon/remote-control.d.ts +32 -9
- package/dist/lib/daemon/remote-control.js +55 -11
- package/dist/lib/daemon/run.d.ts +6 -0
- package/dist/lib/daemon/run.js +43 -2
- package/dist/lib/daemon/tools.d.ts +52 -0
- package/dist/lib/daemon/tools.js +95 -0
- package/dist/lib/daemon/updater.d.ts +23 -15
- package/dist/lib/daemon/updater.js +97 -60
- 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 +1 -1
- package/dist/lib/lanes/outpost-pruner-install.d.ts +37 -0
- package/dist/lib/lanes/outpost-pruner-install.js +226 -0
- package/dist/lib/lanes/outpost-worktree-pruner-flag.d.ts +19 -0
- package/dist/lib/lanes/outpost-worktree-pruner-flag.js +76 -0
- package/dist/lib/lanes/outpost-worktree-pruner.d.ts +60 -0
- package/dist/lib/lanes/outpost-worktree-pruner.js +1135 -0
- package/dist/lib/lanes/store.d.ts +6 -0
- package/dist/lib/lanes/store.js +66 -1
- package/dist/lib/mesh/live/session-events-client.js +3 -2
- package/dist/lib/mesh/project-board.d.ts +7 -0
- package/dist/lib/mesh/project-board.js +29 -1
- package/package.json +1 -1
package/dist/commands/lanes.js
CHANGED
|
@@ -10,6 +10,7 @@ import { randomBytes } from "node:crypto";
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import * as os from "node:os";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
+
import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
|
|
13
14
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
14
15
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
15
16
|
import { resolveProcessFlagGate } from "../lib/flag-registry.js";
|
|
@@ -23,7 +24,7 @@ import { vaultApiFetch } from "../utils/vault-api.js";
|
|
|
23
24
|
import { openMeshTransport } from "../lib/mesh/client.js";
|
|
24
25
|
import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
|
|
25
26
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
26
|
-
import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneState, parseLivenessState, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, withLaneAdmissionLock, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
27
|
+
import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseLivenessState, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, withLaneAdmissionLock, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
27
28
|
import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
|
|
28
29
|
import { questionBodyAbsPath } from "../lib/lanes/questions.js";
|
|
29
30
|
import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
@@ -1119,7 +1120,7 @@ async function assessLaneAdmission(ctx, lane, options) {
|
|
|
1119
1120
|
return admission;
|
|
1120
1121
|
}
|
|
1121
1122
|
function assertLaneRunsHereForAdmission(ctx, lane) {
|
|
1122
|
-
if (
|
|
1123
|
+
if (laneRunsHereForOperatorMutation(ctx, lane))
|
|
1123
1124
|
return;
|
|
1124
1125
|
throw new LanesError("lane_remote", `Lane ${lane.lane_id} is hosted remotely; run the admission command on its Outpost`, { lane_id: lane.lane_id, host: lane.host });
|
|
1125
1126
|
}
|
|
@@ -2382,6 +2383,16 @@ function laneIsHostedHere(ctx, lane) {
|
|
|
2382
2383
|
}
|
|
2383
2384
|
return identity.kind === "id" && identity.id === lane.host.id;
|
|
2384
2385
|
}
|
|
2386
|
+
function laneRunsHereForOperatorMutation(ctx, lane) {
|
|
2387
|
+
if (lane.host.kind === "outpost")
|
|
2388
|
+
return laneIsHostedHere(ctx, lane);
|
|
2389
|
+
return laneRunsHere(lane, reconcileHostFromCtx(ctx));
|
|
2390
|
+
}
|
|
2391
|
+
function assertLaneRunsHereForOperatorMutation(ctx, lane, action) {
|
|
2392
|
+
if (laneRunsHereForOperatorMutation(ctx, lane))
|
|
2393
|
+
return;
|
|
2394
|
+
throw new LanesError("lane_remote", `Cannot ${action} lane ${lane.lane_id} from this host; its state is owned by ${lane.host.kind}:${lane.host.id}`, { lane_id: lane.lane_id, host: lane.host, action });
|
|
2395
|
+
}
|
|
2385
2396
|
async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
2386
2397
|
if (lane.host.kind !== "outpost") {
|
|
2387
2398
|
throw new LanesError("outpost_dispatch_failed", `Lane ${lane.lane_id} has no Outpost host to dispatch to`, { lane_id: lane.lane_id });
|
|
@@ -2896,8 +2907,7 @@ async function runMove(ctx, laneId, opts) {
|
|
|
2896
2907
|
moved = await dispatchRemoteLaneMove(ctx, source, choice.host, choice.account);
|
|
2897
2908
|
}
|
|
2898
2909
|
else {
|
|
2899
|
-
|
|
2900
|
-
throw new LanesError("lane_move_wrong_host", `Move lane ${laneId} from its current host (${source.host.kind}:${source.host.id})`, { lane_id: laneId, lane_host: source.host, current_host: currentHost });
|
|
2910
|
+
throw new LanesError("lane_remote", `Cannot move lane ${laneId} from this host; its state is owned by ${source.host.kind}:${source.host.id}`, { lane_id: laneId, host: source.host, action: "move" });
|
|
2901
2911
|
}
|
|
2902
2912
|
printJson({
|
|
2903
2913
|
ok: true,
|
|
@@ -2912,12 +2922,8 @@ async function runMove(ctx, laneId, opts) {
|
|
|
2912
2922
|
async function commitLaneMove(ctx, laneId, target, account) {
|
|
2913
2923
|
assertPlacementEnabled(ctx);
|
|
2914
2924
|
const hqRoot = ctx.hqRoot();
|
|
2915
|
-
const sourceHost = laneHostFromCtx(ctx);
|
|
2916
|
-
const admissionHost = reconcileHostFromCtx(ctx);
|
|
2917
2925
|
const before = readLane(hqRoot, laneId);
|
|
2918
|
-
|
|
2919
|
-
throw new LanesError("lane_move_wrong_host", `Move lane ${laneId} from its current host (${before.host.kind}:${before.host.id})`, { lane_id: laneId, lane_host: before.host, current_host: sourceHost });
|
|
2920
|
-
}
|
|
2926
|
+
assertLaneRunsHereForOperatorMutation(ctx, before, "move-target");
|
|
2921
2927
|
if (before.host.kind === target.kind && before.host.id === target.id) {
|
|
2922
2928
|
throw new LanesError("lane_move_same_host", `Lane ${laneId} is already on ${target.kind}:${target.id}`, {
|
|
2923
2929
|
lane_id: laneId,
|
|
@@ -2971,12 +2977,7 @@ async function commitLaneMove(ctx, laneId, target, account) {
|
|
|
2971
2977
|
const timeout = target.kind === "outpost" ? readLaneTimeout(hqRoot, laneId) : undefined;
|
|
2972
2978
|
const moved = await withLaneAdmissionLock(hqRoot, before.provider, async () => {
|
|
2973
2979
|
const current = readLane(hqRoot, laneId);
|
|
2974
|
-
|
|
2975
|
-
throw new LanesError("lane_move_conflict", `Lane ${laneId} changed hosts before the move`, {
|
|
2976
|
-
lane_id: laneId,
|
|
2977
|
-
host: current.host,
|
|
2978
|
-
});
|
|
2979
|
-
}
|
|
2980
|
+
assertLaneRunsHereForOperatorMutation(ctx, current, "move-target");
|
|
2980
2981
|
if (!laneCanMoveBeforeStart(current)) {
|
|
2981
2982
|
throw new LanesError(current.state === "queued" ? "lane_move_started" : "lane_not_queued", `Lane ${laneId} started before the move could commit`, { lane_id: laneId, state: current.state });
|
|
2982
2983
|
}
|
|
@@ -3216,17 +3217,15 @@ async function runAdmit(ctx, laneId, opts) {
|
|
|
3216
3217
|
state: lane.state,
|
|
3217
3218
|
});
|
|
3218
3219
|
}
|
|
3220
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "admit");
|
|
3219
3221
|
const host = reconcileHostFromCtx(ctx);
|
|
3220
|
-
if (!laneRunsHere(lane, host)) {
|
|
3221
|
-
printJson({ ok: true, action: "admit", lane_id: laneId, admitted: false, blocked_by: "remote", lane });
|
|
3222
|
-
return;
|
|
3223
|
-
}
|
|
3224
3222
|
if (lane.pickup_validation_pending === true) {
|
|
3225
3223
|
printJson({ ok: true, action: "admit", lane_id: laneId, admitted: false, blocked_by: "pickup_validation", lane });
|
|
3226
3224
|
return;
|
|
3227
3225
|
}
|
|
3228
3226
|
await withLaneAdmissionLock(hqRoot, lane.provider, async () => {
|
|
3229
3227
|
lane = readLane(hqRoot, laneId);
|
|
3228
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "admit");
|
|
3230
3229
|
if (lane.state !== "queued") {
|
|
3231
3230
|
throw new LanesError("lane_not_queued", `Lane ${laneId} changed to ${lane.state} before admission`, {
|
|
3232
3231
|
lane_id: laneId,
|
|
@@ -3572,6 +3571,7 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
3572
3571
|
await dispatchOutpostLaneCommand(ctx, lane, "message", opts);
|
|
3573
3572
|
return;
|
|
3574
3573
|
}
|
|
3574
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "message");
|
|
3575
3575
|
lane = laneWithRecoveredProviderSession(hqRoot, lane);
|
|
3576
3576
|
// An account move is refused on its own terms first: a running lane and a
|
|
3577
3577
|
// provider without session resume get the move's named errors, not the
|
|
@@ -3847,6 +3847,7 @@ async function runResume(ctx, laneId, opts) {
|
|
|
3847
3847
|
await dispatchOutpostLaneCommand(ctx, lane, "resume", opts);
|
|
3848
3848
|
return;
|
|
3849
3849
|
}
|
|
3850
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "resume");
|
|
3850
3851
|
lane = laneWithRecoveredProviderSession(hqRoot, lane);
|
|
3851
3852
|
if (opts.model !== undefined && opts.account === undefined) {
|
|
3852
3853
|
throw new LanesError("model_requires_account", "--model on hq lanes resume is only accepted together with --account", {});
|
|
@@ -4223,6 +4224,7 @@ async function runStoryList(ctx, laneId) {
|
|
|
4223
4224
|
async function runStoryAdd(ctx, laneId, storyId, opts) {
|
|
4224
4225
|
const hqRoot = ctx.hqRoot();
|
|
4225
4226
|
let lane = readLane(hqRoot, laneId);
|
|
4227
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
|
|
4226
4228
|
const projectId = opts.project?.trim() || lane.project_id;
|
|
4227
4229
|
if (!projectId)
|
|
4228
4230
|
throw new LanesError("missing_project", "--project is required", {});
|
|
@@ -4232,7 +4234,10 @@ async function runStoryAdd(ctx, laneId, storyId, opts) {
|
|
|
4232
4234
|
const at = ctx.now().toISOString();
|
|
4233
4235
|
lane = await ensureStoryThread(ctx, lane, projectId);
|
|
4234
4236
|
const before = lane;
|
|
4235
|
-
const next = casUpdateLane(hqRoot, laneId, (current) =>
|
|
4237
|
+
const next = casUpdateLane(hqRoot, laneId, (current) => {
|
|
4238
|
+
assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
|
|
4239
|
+
return addCarriedStory(current, projectId, normalizedStoryId, at);
|
|
4240
|
+
});
|
|
4236
4241
|
await commitStoryTransition(ctx, before, next, projectId, normalizedStoryId, "lane_started", "in_progress");
|
|
4237
4242
|
const finalLane = readLane(hqRoot, laneId);
|
|
4238
4243
|
printJson({
|
|
@@ -4249,6 +4254,7 @@ async function runStoryAdd(ctx, laneId, storyId, opts) {
|
|
|
4249
4254
|
async function runStoryStart(ctx, laneId, storyId, opts) {
|
|
4250
4255
|
const hqRoot = ctx.hqRoot();
|
|
4251
4256
|
let lane = readLane(hqRoot, laneId);
|
|
4257
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
|
|
4252
4258
|
const target = storyCommandTarget(lane, storyId, opts.project);
|
|
4253
4259
|
if (target.story.status === "done") {
|
|
4254
4260
|
throw new LanesError("story_already_done", `Story ${target.projectId}/${target.storyId} is done and cannot be started again`, {
|
|
@@ -4260,7 +4266,10 @@ async function runStoryStart(ctx, laneId, storyId, opts) {
|
|
|
4260
4266
|
const at = ctx.now().toISOString();
|
|
4261
4267
|
lane = await ensureStoryThread(ctx, lane, target.projectId);
|
|
4262
4268
|
const before = lane;
|
|
4263
|
-
const next = casUpdateLane(hqRoot, laneId, (current) =>
|
|
4269
|
+
const next = casUpdateLane(hqRoot, laneId, (current) => {
|
|
4270
|
+
assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
|
|
4271
|
+
return updateCarriedStory(current, target.projectId, target.storyId, "in_progress", at);
|
|
4272
|
+
});
|
|
4264
4273
|
await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_started", "in_progress");
|
|
4265
4274
|
const finalLane = readLane(hqRoot, laneId);
|
|
4266
4275
|
printJson({ ok: true, action: "story.start", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "in_progress", story_warning: storyWarning(finalLane) ?? null, lane: finalLane });
|
|
@@ -4268,6 +4277,7 @@ async function runStoryStart(ctx, laneId, storyId, opts) {
|
|
|
4268
4277
|
async function runStoryBlock(ctx, laneId, storyId, opts) {
|
|
4269
4278
|
const hqRoot = ctx.hqRoot();
|
|
4270
4279
|
let lane = readLane(hqRoot, laneId);
|
|
4280
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
|
|
4271
4281
|
const target = storyCommandTarget(lane, storyId, opts.project);
|
|
4272
4282
|
if (target.story.status === "done") {
|
|
4273
4283
|
throw new LanesError("story_already_done", `Story ${target.projectId}/${target.storyId} is done and cannot be blocked again`, {
|
|
@@ -4282,7 +4292,10 @@ async function runStoryBlock(ctx, laneId, storyId, opts) {
|
|
|
4282
4292
|
const at = ctx.now().toISOString();
|
|
4283
4293
|
lane = await ensureStoryThread(ctx, lane, target.projectId);
|
|
4284
4294
|
const before = lane;
|
|
4285
|
-
const next = casUpdateLane(hqRoot, laneId, (current) =>
|
|
4295
|
+
const next = casUpdateLane(hqRoot, laneId, (current) => {
|
|
4296
|
+
assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
|
|
4297
|
+
return updateCarriedStory(current, target.projectId, target.storyId, "blocked", at, { blocked_reason: reason });
|
|
4298
|
+
});
|
|
4286
4299
|
await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_blocked", "blocked", { blocked_reason: reason });
|
|
4287
4300
|
const finalLane = readLane(hqRoot, laneId);
|
|
4288
4301
|
printJson({ ok: true, action: "story.block", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "blocked", blocked_reason: reason, story_warning: storyWarning(finalLane) ?? null, lane: finalLane });
|
|
@@ -4290,6 +4303,7 @@ async function runStoryBlock(ctx, laneId, storyId, opts) {
|
|
|
4290
4303
|
async function runStoryDone(ctx, laneId, storyId, opts) {
|
|
4291
4304
|
const hqRoot = ctx.hqRoot();
|
|
4292
4305
|
let lane = readLane(hqRoot, laneId);
|
|
4306
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
|
|
4293
4307
|
if (!opts.verified) {
|
|
4294
4308
|
throw new LanesError("senior_verification_required", "Only a senior may mark a story done; pass --verified after reviewing or merging it", { lane_id: laneId });
|
|
4295
4309
|
}
|
|
@@ -4311,10 +4325,13 @@ async function runStoryDone(ctx, laneId, storyId, opts) {
|
|
|
4311
4325
|
const at = ctx.now().toISOString();
|
|
4312
4326
|
lane = await ensureStoryThread(ctx, lane, target.projectId);
|
|
4313
4327
|
const before = lane;
|
|
4314
|
-
const next = casUpdateLane(hqRoot, laneId, (current) =>
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4328
|
+
const next = casUpdateLane(hqRoot, laneId, (current) => {
|
|
4329
|
+
assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
|
|
4330
|
+
return updateCarriedStory(current, target.projectId, target.storyId, "done", at, {
|
|
4331
|
+
verified_at: at,
|
|
4332
|
+
verified_by: current.senior,
|
|
4333
|
+
});
|
|
4334
|
+
});
|
|
4318
4335
|
await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_done", "done", { summary: "Senior verification complete" });
|
|
4319
4336
|
const finalLane = readLane(hqRoot, laneId);
|
|
4320
4337
|
printJson({ ok: true, action: "story.done", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "done", verified: true, story_warning: storyWarning(finalLane) ?? null, lane: finalLane });
|
|
@@ -4354,6 +4371,7 @@ async function runReparent(ctx, laneId, opts) {
|
|
|
4354
4371
|
const nextSenior = parseSeniorRef(opts.senior);
|
|
4355
4372
|
assertSeniorExists(hqRoot, nextSenior, ctx);
|
|
4356
4373
|
const lane = readLane(hqRoot, laneId);
|
|
4374
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "reparent");
|
|
4357
4375
|
if (isTerminalLaneState(lane.state)) {
|
|
4358
4376
|
throw new LanesError("lane_terminal", `Cannot re-parent a lane in terminal state ${lane.state}`, { lane_id: laneId, state: lane.state });
|
|
4359
4377
|
}
|
|
@@ -4378,6 +4396,7 @@ async function runReparent(ctx, laneId, opts) {
|
|
|
4378
4396
|
continue;
|
|
4379
4397
|
if (!walkHitsLane(hqRoot, other.senior, laneId, ctx))
|
|
4380
4398
|
continue;
|
|
4399
|
+
assertLaneRunsHereForOperatorMutation(ctx, other, "reparent descendant of");
|
|
4381
4400
|
const oldDescendantDepth = derivedLaneDepth(hqRoot, other.senior, ctx);
|
|
4382
4401
|
const descendantDepth = derivedLaneDepth(hqRoot, other.senior, ctx, override);
|
|
4383
4402
|
if (descendantDepth > MAX_LANE_DEPTH) {
|
|
@@ -4395,6 +4414,7 @@ async function runReparent(ctx, laneId, opts) {
|
|
|
4395
4414
|
}
|
|
4396
4415
|
const at = ctx.now().toISOString();
|
|
4397
4416
|
const updated = casUpdateLane(hqRoot, laneId, (current) => {
|
|
4417
|
+
assertLaneRunsHereForOperatorMutation(ctx, current, "reparent");
|
|
4398
4418
|
const already = current.senior_history.some((entry) => entry.at === at && entry.reason === (opts.reason ?? "reparent"));
|
|
4399
4419
|
if (already && current.depth === newDepth)
|
|
4400
4420
|
return current;
|
|
@@ -4413,6 +4433,7 @@ async function runReparent(ctx, laneId, opts) {
|
|
|
4413
4433
|
});
|
|
4414
4434
|
for (const descendant of descendants) {
|
|
4415
4435
|
casUpdateLane(hqRoot, descendant.laneId, (current) => {
|
|
4436
|
+
assertLaneRunsHereForOperatorMutation(ctx, current, "reparent descendant of");
|
|
4416
4437
|
if (current.depth === descendant.depth)
|
|
4417
4438
|
return current;
|
|
4418
4439
|
return { ...current, depth: descendant.depth };
|
|
@@ -4487,14 +4508,226 @@ async function runLogs(ctx, laneId, opts) {
|
|
|
4487
4508
|
export function stopCommittedState(state) {
|
|
4488
4509
|
return state === "queued" ? "cancelled" : "killed";
|
|
4489
4510
|
}
|
|
4511
|
+
function parseConfirmedOutpostStopLane(raw, laneId, outpostId, commandId) {
|
|
4512
|
+
let remoteLane;
|
|
4513
|
+
try {
|
|
4514
|
+
remoteLane = parseLaneRecord(raw);
|
|
4515
|
+
}
|
|
4516
|
+
catch (err) {
|
|
4517
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} returned an invalid lane snapshot for stop ${laneId}`, {
|
|
4518
|
+
lane_id: laneId,
|
|
4519
|
+
outpost_id: outpostId,
|
|
4520
|
+
command_id: commandId,
|
|
4521
|
+
cause: err instanceof Error ? err.message : String(err),
|
|
4522
|
+
});
|
|
4523
|
+
}
|
|
4524
|
+
if (remoteLane.lane_id !== laneId ||
|
|
4525
|
+
remoteLane.host.kind !== "outpost" ||
|
|
4526
|
+
remoteLane.host.id !== outpostId ||
|
|
4527
|
+
!isTerminalLaneState(remoteLane.state)) {
|
|
4528
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return its terminal lane ${laneId}`, {
|
|
4529
|
+
lane_id: laneId,
|
|
4530
|
+
outpost_id: outpostId,
|
|
4531
|
+
command_id: commandId,
|
|
4532
|
+
owner_lane_id: remoteLane.lane_id,
|
|
4533
|
+
owner_host: remoteLane.host,
|
|
4534
|
+
owner_state: remoteLane.state,
|
|
4535
|
+
});
|
|
4536
|
+
}
|
|
4537
|
+
return remoteLane;
|
|
4538
|
+
}
|
|
4539
|
+
function parseRemoteCancelledQuestionIds(value, laneId, outpostId, commandId) {
|
|
4540
|
+
if (!Array.isArray(value) ||
|
|
4541
|
+
value.some((id) => typeof id !== "string" || id.trim() === "") ||
|
|
4542
|
+
new Set(value).size !== value.length) {
|
|
4543
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} returned invalid cancelled question ids for lane ${laneId}`, { lane_id: laneId, outpost_id: outpostId, command_id: commandId });
|
|
4544
|
+
}
|
|
4545
|
+
return value;
|
|
4546
|
+
}
|
|
4547
|
+
async function readOutpostTerminalLane(ctx, laneId, outpostId, stopCommandId) {
|
|
4548
|
+
let dispatched;
|
|
4549
|
+
try {
|
|
4550
|
+
dispatched = await ctx.dispatchOutpostExec({
|
|
4551
|
+
outpostId,
|
|
4552
|
+
command: buildOutpostLaneCommand({ action: "show", laneId }),
|
|
4553
|
+
timeoutSeconds: 120,
|
|
4554
|
+
waitMs: 120_000,
|
|
4555
|
+
});
|
|
4556
|
+
}
|
|
4557
|
+
catch (err) {
|
|
4558
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Could not read terminal lane ${laneId} from Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, {
|
|
4559
|
+
lane_id: laneId,
|
|
4560
|
+
outpost_id: outpostId,
|
|
4561
|
+
command_id: stopCommandId,
|
|
4562
|
+
sync_command_id: null,
|
|
4563
|
+
});
|
|
4564
|
+
}
|
|
4565
|
+
const commandId = dispatched.commandId?.trim();
|
|
4566
|
+
if (dispatched.done !== true ||
|
|
4567
|
+
!commandId ||
|
|
4568
|
+
dispatched.status !== "Success" ||
|
|
4569
|
+
dispatched.exitCode !== 0) {
|
|
4570
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not confirm a lane read after stop ${laneId}`, {
|
|
4571
|
+
lane_id: laneId,
|
|
4572
|
+
outpost_id: outpostId,
|
|
4573
|
+
command_id: stopCommandId,
|
|
4574
|
+
sync_command_id: commandId || null,
|
|
4575
|
+
status: dispatched.status,
|
|
4576
|
+
done: dispatched.done ?? false,
|
|
4577
|
+
exit_code: dispatched.exitCode,
|
|
4578
|
+
});
|
|
4579
|
+
}
|
|
4580
|
+
let remote;
|
|
4581
|
+
try {
|
|
4582
|
+
remote = parseRemoteJson(dispatched.stdout, "Outpost lane show");
|
|
4583
|
+
}
|
|
4584
|
+
catch (err) {
|
|
4585
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} returned an invalid lane read after stop ${laneId}`, {
|
|
4586
|
+
lane_id: laneId,
|
|
4587
|
+
outpost_id: outpostId,
|
|
4588
|
+
command_id: stopCommandId,
|
|
4589
|
+
sync_command_id: commandId,
|
|
4590
|
+
cause: err instanceof Error ? err.message : String(err),
|
|
4591
|
+
});
|
|
4592
|
+
}
|
|
4593
|
+
if (remote?.ok !== true || remote.action !== "show") {
|
|
4594
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return lane ${laneId} after stop`, {
|
|
4595
|
+
lane_id: laneId,
|
|
4596
|
+
outpost_id: outpostId,
|
|
4597
|
+
command_id: stopCommandId,
|
|
4598
|
+
sync_command_id: commandId,
|
|
4599
|
+
});
|
|
4600
|
+
}
|
|
4601
|
+
return {
|
|
4602
|
+
lane: parseConfirmedOutpostStopLane(remote.lane, laneId, outpostId, commandId),
|
|
4603
|
+
commandId,
|
|
4604
|
+
};
|
|
4605
|
+
}
|
|
4606
|
+
async function dispatchOutpostLaneStop(ctx, lane) {
|
|
4607
|
+
if (lane.host.kind !== "outpost") {
|
|
4608
|
+
throw new LanesError("outpost_dispatch_failed", `Lane ${lane.lane_id} has no Outpost host to dispatch stop to`, { lane_id: lane.lane_id });
|
|
4609
|
+
}
|
|
4610
|
+
const outpostId = lane.host.id;
|
|
4611
|
+
let dispatched;
|
|
4612
|
+
try {
|
|
4613
|
+
dispatched = await ctx.dispatchOutpostExec({
|
|
4614
|
+
outpostId,
|
|
4615
|
+
command: buildOutpostLaneCommand({ action: "stop", laneId: lane.lane_id }),
|
|
4616
|
+
timeoutSeconds: 120,
|
|
4617
|
+
waitMs: 120_000,
|
|
4618
|
+
});
|
|
4619
|
+
}
|
|
4620
|
+
catch (err) {
|
|
4621
|
+
throw new LanesError("outpost_dispatch_failed", `Could not dispatch stop for lane ${lane.lane_id} to Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { lane_id: lane.lane_id, outpost_id: outpostId });
|
|
4622
|
+
}
|
|
4623
|
+
const commandId = dispatched.commandId?.trim();
|
|
4624
|
+
if (dispatched.done !== true || !commandId) {
|
|
4625
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return a completed stop command for lane ${lane.lane_id}`, {
|
|
4626
|
+
lane_id: lane.lane_id,
|
|
4627
|
+
outpost_id: outpostId,
|
|
4628
|
+
command_id: commandId || null,
|
|
4629
|
+
status: dispatched.status,
|
|
4630
|
+
done: dispatched.done ?? false,
|
|
4631
|
+
exit_code: dispatched.exitCode,
|
|
4632
|
+
});
|
|
4633
|
+
}
|
|
4634
|
+
const remote = parseRemoteJson(dispatched.stdout, "Outpost lane stop");
|
|
4635
|
+
let state;
|
|
4636
|
+
let alreadyTerminal = false;
|
|
4637
|
+
let ownerLane;
|
|
4638
|
+
let cancelledQuestions;
|
|
4639
|
+
let syncCommandId;
|
|
4640
|
+
if (remote?.error === "already_terminal") {
|
|
4641
|
+
if (remote.lane_id !== lane.lane_id ||
|
|
4642
|
+
!isTerminalLaneState(remote.state) ||
|
|
4643
|
+
(dispatched.status !== "Success" && dispatched.status !== "Failed") ||
|
|
4644
|
+
dispatched.exitCode === null ||
|
|
4645
|
+
dispatched.exitCode === 0) {
|
|
4646
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} returned an invalid already-terminal result for lane ${lane.lane_id}`, { lane_id: lane.lane_id, outpost_id: outpostId, command_id: commandId });
|
|
4647
|
+
}
|
|
4648
|
+
state = remote.state;
|
|
4649
|
+
alreadyTerminal = true;
|
|
4650
|
+
const owner = await readOutpostTerminalLane(ctx, lane.lane_id, outpostId, commandId);
|
|
4651
|
+
if (owner.lane.state !== state) {
|
|
4652
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} returned a different terminal state for lane ${lane.lane_id}`, {
|
|
4653
|
+
lane_id: lane.lane_id,
|
|
4654
|
+
outpost_id: outpostId,
|
|
4655
|
+
command_id: commandId,
|
|
4656
|
+
sync_command_id: owner.commandId,
|
|
4657
|
+
stop_state: state,
|
|
4658
|
+
owner_state: owner.lane.state,
|
|
4659
|
+
});
|
|
4660
|
+
}
|
|
4661
|
+
ownerLane = owner.lane;
|
|
4662
|
+
cancelledQuestions = [];
|
|
4663
|
+
syncCommandId = owner.commandId;
|
|
4664
|
+
}
|
|
4665
|
+
else if (remote?.error !== undefined) {
|
|
4666
|
+
throw new LanesError("outpost_remote_command_failed", typeof remote.message === "string"
|
|
4667
|
+
? remote.message
|
|
4668
|
+
: `Outpost ${outpostId} rejected the stop command for lane ${lane.lane_id}`, {
|
|
4669
|
+
lane_id: lane.lane_id,
|
|
4670
|
+
outpost_id: outpostId,
|
|
4671
|
+
command_id: commandId,
|
|
4672
|
+
remote_error: remote.error,
|
|
4673
|
+
});
|
|
4674
|
+
}
|
|
4675
|
+
else {
|
|
4676
|
+
const remoteLane = remote?.lane;
|
|
4677
|
+
const remoteLaneRecord = remoteLane !== null &&
|
|
4678
|
+
typeof remoteLane === "object" &&
|
|
4679
|
+
!Array.isArray(remoteLane)
|
|
4680
|
+
? remoteLane
|
|
4681
|
+
: null;
|
|
4682
|
+
if (dispatched.status !== "Success" ||
|
|
4683
|
+
dispatched.exitCode !== 0 ||
|
|
4684
|
+
remote?.ok !== true ||
|
|
4685
|
+
remote.action !== "stop" ||
|
|
4686
|
+
remoteLaneRecord?.lane_id !== lane.lane_id ||
|
|
4687
|
+
(remoteLaneRecord.state !== "cancelled" && remoteLaneRecord.state !== "killed")) {
|
|
4688
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} completed exec without confirming the stop for lane ${lane.lane_id}`, {
|
|
4689
|
+
lane_id: lane.lane_id,
|
|
4690
|
+
outpost_id: outpostId,
|
|
4691
|
+
command_id: commandId,
|
|
4692
|
+
status: dispatched.status,
|
|
4693
|
+
exit_code: dispatched.exitCode,
|
|
4694
|
+
});
|
|
4695
|
+
}
|
|
4696
|
+
ownerLane = parseConfirmedOutpostStopLane(remoteLaneRecord, lane.lane_id, outpostId, commandId);
|
|
4697
|
+
state = ownerLane.state;
|
|
4698
|
+
cancelledQuestions = parseRemoteCancelledQuestionIds(remote.cancelled_questions, lane.lane_id, outpostId, commandId);
|
|
4699
|
+
}
|
|
4700
|
+
const updated = syncLaneRecordFromOwner(ctx.hqRoot(), lane.lane_id, outpostId, ownerLane);
|
|
4701
|
+
printJson({
|
|
4702
|
+
ok: true,
|
|
4703
|
+
action: "stop",
|
|
4704
|
+
lane: updated,
|
|
4705
|
+
cancelled_questions: cancelledQuestions,
|
|
4706
|
+
outpost_id: outpostId,
|
|
4707
|
+
outpost_command_id: commandId,
|
|
4708
|
+
...(syncCommandId ? { outpost_sync_command_id: syncCommandId } : {}),
|
|
4709
|
+
outpost_result: {
|
|
4710
|
+
state,
|
|
4711
|
+
already_terminal: alreadyTerminal,
|
|
4712
|
+
status: dispatched.status,
|
|
4713
|
+
exit_code: dispatched.exitCode,
|
|
4714
|
+
},
|
|
4715
|
+
});
|
|
4716
|
+
}
|
|
4490
4717
|
async function runStop(ctx, laneId) {
|
|
4491
4718
|
const hqRoot = ctx.hqRoot();
|
|
4492
4719
|
const lane = readLane(hqRoot, laneId);
|
|
4720
|
+
if (lane.host.kind === "outpost" && !laneRunsHereForOperatorMutation(ctx, lane)) {
|
|
4721
|
+
await dispatchOutpostLaneStop(ctx, lane);
|
|
4722
|
+
return;
|
|
4723
|
+
}
|
|
4724
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "stop");
|
|
4493
4725
|
if (isTerminalLaneState(lane.state)) {
|
|
4494
4726
|
throw new LanesError("already_terminal", `Lane ${laneId} is already ${lane.state}`, { lane_id: laneId, state: lane.state });
|
|
4495
4727
|
}
|
|
4496
4728
|
const at = ctx.now().toISOString();
|
|
4497
4729
|
const updated = casUpdateLane(hqRoot, laneId, (current) => {
|
|
4730
|
+
assertLaneRunsHereForOperatorMutation(ctx, current, "stop");
|
|
4498
4731
|
if (isTerminalLaneState(current.state)) {
|
|
4499
4732
|
throw new LanesError("already_terminal", `Lane ${laneId} is already ${current.state}`, { lane_id: laneId, state: current.state });
|
|
4500
4733
|
}
|
|
@@ -4531,6 +4764,7 @@ async function runAsk(ctx, opts) {
|
|
|
4531
4764
|
throw new LanesError("missing_question", "--question is required", {});
|
|
4532
4765
|
}
|
|
4533
4766
|
const lane = readLane(hqRoot, laneId);
|
|
4767
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "ask");
|
|
4534
4768
|
const optionList = Array.isArray(opts.option)
|
|
4535
4769
|
? opts.option
|
|
4536
4770
|
: opts.option
|
|
@@ -4659,6 +4893,10 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
|
4659
4893
|
const question = readQuestion(hqRoot, questionId);
|
|
4660
4894
|
throwIfQuestionNotPending(question);
|
|
4661
4895
|
await yieldEventLoop();
|
|
4896
|
+
const askingLane = readLane(hqRoot, laneIdForAnswerDelivery(question));
|
|
4897
|
+
if (laneAcceptsDropboxDelivery(askingLane)) {
|
|
4898
|
+
assertLaneRunsHereForOperatorMutation(ctx, askingLane, "deliver an answer to");
|
|
4899
|
+
}
|
|
4662
4900
|
const body = requireQuestionBody(hqRoot, question);
|
|
4663
4901
|
// A trimmed-empty --freeform is absent: it must not win over --choice and
|
|
4664
4902
|
// must not become a freeform answer of "".
|
|
@@ -4700,7 +4938,7 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
|
4700
4938
|
},
|
|
4701
4939
|
};
|
|
4702
4940
|
});
|
|
4703
|
-
const delivery = deliverAnswerToAskingLane(
|
|
4941
|
+
const delivery = deliverAnswerToAskingLane(ctx, updated, choice, at);
|
|
4704
4942
|
printJson({
|
|
4705
4943
|
ok: true,
|
|
4706
4944
|
action: "questions.answer",
|
|
@@ -4729,13 +4967,15 @@ function writeAnswerDeliveryFailed(hqRoot, laneId, questionId, err, at) {
|
|
|
4729
4967
|
* Record is already durable. Delivery is best-effort onto the asking lane's
|
|
4730
4968
|
* drop box (spec §6.5). Target is question.lane_id, never a project/story lookup.
|
|
4731
4969
|
*/
|
|
4732
|
-
function deliverAnswerToAskingLane(
|
|
4970
|
+
function deliverAnswerToAskingLane(ctx, question, choice, at) {
|
|
4971
|
+
const hqRoot = ctx.hqRoot();
|
|
4733
4972
|
const laneId = laneIdForAnswerDelivery(question);
|
|
4734
4973
|
try {
|
|
4735
4974
|
const lane = readLane(hqRoot, laneId);
|
|
4736
4975
|
if (!laneAcceptsDropboxDelivery(lane)) {
|
|
4737
4976
|
return { status: "recorded_only" };
|
|
4738
4977
|
}
|
|
4978
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "deliver an answer to");
|
|
4739
4979
|
appendDropboxMessage(hqRoot, laneId, answerDropboxMessage(question, choice, at, mintMessageId()));
|
|
4740
4980
|
return { status: "dropped" };
|
|
4741
4981
|
}
|
|
@@ -5146,6 +5386,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
5146
5386
|
const lanes = parent
|
|
5147
5387
|
.command("lanes", { hidden: true })
|
|
5148
5388
|
.description("Create, supervise, and steer worker lanes; routed decision queue");
|
|
5389
|
+
registerOutpostPrunerCommand(lanes);
|
|
5149
5390
|
lanes
|
|
5150
5391
|
.command("create")
|
|
5151
5392
|
.description("Register a lane, run admission, and return immediately")
|
package/dist/commands/mesh.js
CHANGED
|
@@ -14,7 +14,7 @@ import chalk from "chalk";
|
|
|
14
14
|
import { loadCachedTokens, isMachineIdentity } from "../utils/cognito-session.js";
|
|
15
15
|
import * as readline from "node:readline/promises";
|
|
16
16
|
import { STORY_STATUSES, appendThreadEvent, ensureProjectThread, eventPayload, getProjectView, listActiveMembershipCompanies, listActiveThreads, patchStory, putProjectView, registerMeshProject, resolveActiveMembershipCompany, resolveMeshPrincipalUid, warmMeshConversationCache, } from "../lib/mesh/api.js";
|
|
17
|
-
import { closeSupersededProjectThread, ensureMissingBoardStories, ensureProjectBoard } from "../lib/mesh/project-board.js";
|
|
17
|
+
import { closeSupersededProjectThread, ensureMissingBoardStories, ensureProjectBoard, formatStoryStatusChanges, mergeProjectStories, } from "../lib/mesh/project-board.js";
|
|
18
18
|
import { buildPreflightFixtures } from "../lib/work-context/preflight-contract.js";
|
|
19
19
|
import { createCandidatesFetcher, createMigratePoster, createOrganizePoster, createWorkSessionDeliverer, fetchCompanyLive, formatCompanyLiveTable, openMeshTransport, probeMigrationCapabilityForMemberships, requireToken, } from "../lib/mesh/client.js";
|
|
20
20
|
import { clearDefaultCompany, isDefaultCompanyDisabled, readDeviceConfig, recordMigrationCapabilitySnapshot, setRepairHeldWithDefault, setDefaultCompany, } from "../lib/work-context/config.js";
|
|
@@ -307,23 +307,6 @@ function httpStatus(err) {
|
|
|
307
307
|
}
|
|
308
308
|
return undefined;
|
|
309
309
|
}
|
|
310
|
-
function mergeProjectStories(current, planned) {
|
|
311
|
-
const currentById = new Map(current.map((story) => [story.id, story]));
|
|
312
|
-
const plannedIds = new Set(planned.map((story) => story.id));
|
|
313
|
-
const merged = planned.map((story) => {
|
|
314
|
-
const live = currentById.get(story.id);
|
|
315
|
-
if (!live)
|
|
316
|
-
return story;
|
|
317
|
-
const status = live.status || story.status;
|
|
318
|
-
return {
|
|
319
|
-
...story,
|
|
320
|
-
...(status ? { status } : {}),
|
|
321
|
-
passes: status === "done" ? true : typeof live.passes === "boolean" ? live.passes : story.passes,
|
|
322
|
-
...(live.assigneeUid ? { assigneeUid: live.assigneeUid } : {}),
|
|
323
|
-
};
|
|
324
|
-
});
|
|
325
|
-
return [...merged, ...current.filter((story) => !plannedIds.has(story.id))];
|
|
326
|
-
}
|
|
327
310
|
function mergeProjectRepos(current, planned) {
|
|
328
311
|
const plannedPaths = new Set(planned.map((repo) => repo.path));
|
|
329
312
|
return [...planned, ...current.filter((repo) => !plannedPaths.has(repo.path))];
|
|
@@ -386,6 +369,10 @@ async function runProjectSet(projectId, opts) {
|
|
|
386
369
|
registration = ensured.registration;
|
|
387
370
|
}
|
|
388
371
|
else if (opts.name !== undefined || opts.description !== undefined || source !== undefined || explicitRepos !== undefined) {
|
|
372
|
+
const stories = source
|
|
373
|
+
? mergeProjectStories(current.stories, source.stories, { resetStatus: Boolean(opts.resetStatus) })
|
|
374
|
+
: current.stories;
|
|
375
|
+
const statusChangeLine = source ? formatStoryStatusChanges(current.stories, stories) : undefined;
|
|
389
376
|
try {
|
|
390
377
|
view = await putProjectView(token, company.companyUid, projectId, {
|
|
391
378
|
name: opts.name?.trim() || source?.name || current.name,
|
|
@@ -394,7 +381,7 @@ async function runProjectSet(projectId, opts) {
|
|
|
394
381
|
: source?.description !== undefined
|
|
395
382
|
? { description: source.description }
|
|
396
383
|
: { description: current.description }),
|
|
397
|
-
stories
|
|
384
|
+
stories,
|
|
398
385
|
repos: explicitRepos ?? source?.repos ?? current.repos,
|
|
399
386
|
...(current.version !== undefined ? { expectedVersion: current.version } : {}),
|
|
400
387
|
});
|
|
@@ -409,6 +396,12 @@ async function runProjectSet(projectId, opts) {
|
|
|
409
396
|
}
|
|
410
397
|
throw err;
|
|
411
398
|
}
|
|
399
|
+
if (statusChangeLine) {
|
|
400
|
+
if (opts.json)
|
|
401
|
+
console.error(statusChangeLine);
|
|
402
|
+
else
|
|
403
|
+
console.log(statusChangeLine);
|
|
404
|
+
}
|
|
412
405
|
}
|
|
413
406
|
else {
|
|
414
407
|
view = current;
|
|
@@ -1816,6 +1809,7 @@ export function registerMeshCommand(program) {
|
|
|
1816
1809
|
.option("--repo <path[@branch]>", "Repository binding (repeatable)", collectRepeatable)
|
|
1817
1810
|
.option("--channel <channelId>", "Bind the project to an existing company or project channel")
|
|
1818
1811
|
.option("--create", "Create the Board view when the GET is a 404")
|
|
1812
|
+
.option("--reset-status", "Overwrite live story status from the PRD (prints a one-line summary of changes)")
|
|
1819
1813
|
.option("--json", "Print machine-readable JSON")
|
|
1820
1814
|
.action((projectId, opts) => wrap(() => runProjectSet(projectId, opts))());
|
|
1821
1815
|
project
|