@zq-silk/yui 0.6.0 → 0.6.2
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/README.md +5 -5
- package/dist/agent/managedRuntimeEnvironment.js +2 -1
- package/dist/cli/commandCatalog.js +251 -13
- package/dist/cli/updateOrchestrator.js +8 -0
- package/dist/cli/updatePorts.js +76 -22
- package/dist/cli.js +264 -20
- package/dist/commands/configCommands.js +83 -9
- package/dist/commands/controllerCommands.js +103 -0
- package/dist/commands/deliveryGuardPreflight.js +30 -0
- package/dist/commands/durableJobCommands.js +231 -0
- package/dist/commands/executionAuditCommands.js +193 -0
- package/dist/commands/grantCommands.js +374 -0
- package/dist/commands/projectCommands.js +119 -81
- package/dist/commands/releaseCommands.js +444 -0
- package/dist/commands/resourcesCommands.js +274 -0
- package/dist/commands/sessionCommands.js +104 -0
- package/dist/commands/taskActor.js +117 -0
- package/dist/commands/taskChangeSetCommands.js +60 -0
- package/dist/commands/taskCommands.js +618 -202
- package/dist/commands/taskCompletionGate.js +78 -1
- package/dist/commands/taskContextCommand.js +33 -6
- package/dist/commands/taskInputCommands.js +1 -1
- package/dist/commands/taskIntegrationCommands.js +136 -33
- package/dist/commands/taskIntegrationQueueCommands.js +228 -0
- package/dist/commands/taskNextActionCommand.js +100 -0
- package/dist/commands/taskOverlapCommands.js +120 -0
- package/dist/commands/taskOverviewCommand.js +36 -8
- package/dist/commands/telemetryCommands.js +330 -0
- package/dist/commands/workflowCommands.js +415 -0
- package/dist/config/yuiConfig.js +62 -0
- package/dist/controller/clientRuntime.js +42 -1
- package/dist/controller/controller.js +402 -56
- package/dist/controller/controllerMain.js +25 -2
- package/dist/controller/domainIdentity.js +16 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
- package/dist/controller/handoverCandidate.js +168 -0
- package/dist/controller/jobClient.js +102 -0
- package/dist/controller/jobControl.js +613 -0
- package/dist/controller/jobSupervisor.js +498 -0
- package/dist/controller/providerHookRunFence.js +34 -5
- package/dist/controller/resourceCleanupLinux.js +18 -9
- package/dist/controller/resourceInventoryLinux.js +90 -39
- package/dist/controller/runtime.js +165 -15
- package/dist/controller/runtimeEventInbox.js +234 -57
- package/dist/controller/runtimeEventProcessor.js +297 -58
- package/dist/controller/sessionOwnerReconciliation.js +321 -0
- package/dist/core/controllerServer.js +416 -27
- package/dist/core/controllerTelemetry.js +167 -0
- package/dist/doctor/doctor.js +113 -16
- package/dist/domain/validation.js +9 -0
- package/dist/execution/executionGroup.js +40 -3
- package/dist/executor/agentExecutor.js +6 -3
- package/dist/executor/effectiveLaunch.js +52 -0
- package/dist/executor/executorRegistry.js +50 -0
- package/dist/executor/fileRoleLaunchPlanner.js +61 -6
- package/dist/grant/capabilityGrant.js +282 -0
- package/dist/integration/changeSet.js +16 -3
- package/dist/integration/changeSetManifest.js +46 -0
- package/dist/integration/gitIntegrationService.js +528 -147
- package/dist/integration/integrationAttempt.js +54 -5
- package/dist/integration/integrationQueueEntry.js +221 -0
- package/dist/integration/integrationQueueService.js +955 -0
- package/dist/integration/manifestTags.js +99 -0
- package/dist/integration/overlapDiagnostics.js +211 -0
- package/dist/job/durableJob.js +449 -0
- package/dist/job/jobRunner.js +350 -0
- package/dist/lifecycle/exactRunTerminalization.js +24 -2
- package/dist/lifecycle/providerErrorClass.js +126 -0
- package/dist/message/message.js +16 -3
- package/dist/observability/executionAudit.js +545 -0
- package/dist/observability/faultClassification.js +160 -0
- package/dist/observability/runtimeIdentity.js +367 -0
- package/dist/release/fakeReleasePorts.js +55 -0
- package/dist/release/releaseHandover.js +475 -0
- package/dist/release/releaseIdempotencyStore.js +165 -0
- package/dist/release/releaseWorkflow.js +459 -0
- package/dist/release/releaseWorkflowEngine.js +688 -0
- package/dist/release/releaseWorkflowPorts.js +1720 -0
- package/dist/release/runtimeRelease.js +495 -0
- package/dist/release/workflowFileLock.js +218 -0
- package/dist/repository/gitWorkspace.js +177 -1
- package/dist/repository/projectMaintenanceLock.js +315 -0
- package/dist/repository/taskWorkspaceCoordinator.js +87 -17
- package/dist/repository/taskWorkspacePreparer.js +1091 -517
- package/dist/resources/autoResourceGc.js +116 -0
- package/dist/resources/liveReferences.js +574 -0
- package/dist/resources/resourceDiscovery.js +477 -0
- package/dist/resources/resourceGc.js +645 -0
- package/dist/resources/resourceRegistrar.js +256 -0
- package/dist/resources/resourceRegistry.js +150 -0
- package/dist/resources/resourceRegistryStore.js +41 -0
- package/dist/resources/resourceTypes.js +42 -0
- package/dist/resources/sqliteResourceRegistry.js +111 -0
- package/dist/review/reviewConfig.js +10 -0
- package/dist/review/reviewFinding.js +240 -0
- package/dist/review/reviewFindingLedger.js +545 -0
- package/dist/review/reviewOutcomeClassifier.js +61 -0
- package/dist/review/reviewRound.js +56 -4
- package/dist/run/agentRun.js +80 -4
- package/dist/run/providerRetry.js +84 -0
- package/dist/run/providerRetryConfig.js +63 -0
- package/dist/run/yieldReceipt.js +65 -0
- package/dist/runtime/exactControlPlane.js +79 -2
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/sessionOwnerIdentity.js +269 -0
- package/dist/runtime/sessionOwnerRegistry.js +132 -0
- package/dist/runtime/sessionReconciliation.js +93 -0
- package/dist/runtime/sessionTerminationGuard.js +211 -0
- package/dist/runtime/taskRuntimeIsolation.js +13 -0
- package/dist/runtime/tmuxAdapters.js +34 -1
- package/dist/scheduler/actionability.js +155 -0
- package/dist/scheduler/activeRoleRunDelivery.js +14 -5
- package/dist/scheduler/activeTaskProgress.js +60 -0
- package/dist/scheduler/leaderWakeupProcessor.js +22 -11
- package/dist/scheduler/roleRunStall.js +135 -29
- package/dist/scheduler/taskExecutionProjection.js +11 -0
- package/dist/storage/compatibleTaskStore.js +112 -5
- package/dist/storage/migration/productionRegistry.js +736 -1
- package/dist/storage/sqliteSchema.js +264 -3
- package/dist/storage/sqliteStore.js +487 -13
- package/dist/storage/storeRpc.js +21 -0
- package/dist/storage/taskStore.js +974 -21
- package/dist/storage/upgrade/homeClassification.js +120 -2
- package/dist/storage/upgrade/migrationReceipt.js +67 -0
- package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
- package/dist/storage/upgrade/recordVersions.js +10 -1
- package/dist/storage/upgrade/sqliteMigrationTarget.js +58 -6
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
- package/dist/storage/upgrade/sqliteStateMigration.js +258 -2
- package/dist/storage/upgrade/upgradeOrchestrator.js +482 -16
- package/dist/task/deliveryGuard.js +226 -0
- package/dist/task/nextAction.js +738 -0
- package/dist/task/repairWave.js +137 -0
- package/dist/task/taskRecordReference.js +6 -1
- package/dist/telemetry/sqliteTelemetryStore.js +387 -0
- package/dist/telemetry/telemetryCompaction.js +251 -0
- package/dist/telemetry/telemetryConfig.js +64 -0
- package/dist/telemetry/telemetryRouter.js +32 -0
- package/dist/telemetry/telemetryStore.js +19 -0
- package/dist/telemetry/telemetryWiring.js +33 -0
- package/dist/tmux/tmuxManager.js +20 -1
- package/dist/tmux/tmuxSocketEndpoint.js +20 -0
- package/dist/verification/gateArtifact.js +216 -0
- package/dist/verification/gateArtifactStore.js +87 -0
- package/dist/verification/verificationGateService.js +414 -0
- package/dist/verification/verificationPlan.js +308 -0
- package/dist/workspace/gitChangeSetCapture.js +12 -2
- package/dist/workspace/workItemChangeSetManager.js +60 -3
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +8 -0
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
3
4
|
import { createInterface } from "node:readline/promises";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { isDeepStrictEqual } from "node:util";
|
|
@@ -20,15 +21,29 @@ import { nativeAgentEnvironmentNames } from "./agent/launchEnvironment.js";
|
|
|
20
21
|
import { runAgentCommand } from "./commands/agentCommands.js";
|
|
21
22
|
import { runGlobalRoleCommand } from "./commands/globalRoleCommands.js";
|
|
22
23
|
import { runConfigCommand } from "./commands/configCommands.js";
|
|
23
|
-
import { parseControllerCleanupOptions, parseControllerStatusOptions, renderControllerResourceStatus, runInteractiveControllerCleanup } from "./commands/controllerCommands.js";
|
|
24
|
+
import { parseControllerCleanupOptions, parseControllerStatusOptions, parseControllerRuntimeSnapshot, renderControllerResourceStatus, renderRuntimeIdentitySection, summarizeDurablePhysicalMismatch, runInteractiveControllerCleanup } from "./commands/controllerCommands.js";
|
|
25
|
+
import { parseExecutionAuditOptions, runExecutionAuditCommand } from "./commands/executionAuditCommands.js";
|
|
26
|
+
import { parseSessionReconcileOptions, runSessionReconcileCommand } from "./commands/sessionCommands.js";
|
|
27
|
+
import { SessionOwnerReconciliation } from "./controller/sessionOwnerReconciliation.js";
|
|
24
28
|
import { runJobCommand } from "./commands/jobCommands.js";
|
|
29
|
+
import { runDurableJobCommand } from "./commands/durableJobCommands.js";
|
|
30
|
+
import { runTelemetryCommand } from "./commands/telemetryCommands.js";
|
|
31
|
+
import { runResourcesCommand } from "./commands/resourcesCommands.js";
|
|
25
32
|
import { applyOperatorSessionControl, runOperatorCommand } from "./commands/operatorCommands.js";
|
|
26
33
|
import { runProjectCommand } from "./commands/projectCommands.js";
|
|
27
34
|
import { runProfileCommand } from "./commands/profileCommands.js";
|
|
28
35
|
import { dispatchPreparedReviewRound, failPendingReviewRound, RESUMED_PENDING_FINAL_REVIEW, TERMINALIZED_LEADER_BEFORE_FINAL_REVIEW, TaskFinalReviewDispatchDriftError, preserveReviewRoundWorkspace, parseTaskCompletionRequest, preflightTaskCompletion, runTaskCommand, normalizedExecutionLanePlan, validateTaskArchiveRequest } from "./commands/taskCommands.js";
|
|
29
36
|
import { taskActor } from "./commands/taskActor.js";
|
|
30
37
|
import { runTaskIntegrationCommand } from "./commands/taskIntegrationCommands.js";
|
|
38
|
+
import { runTaskChangeSetCommand } from "./commands/taskChangeSetCommands.js";
|
|
39
|
+
import { runTaskOverlapCommand } from "./commands/taskOverlapCommands.js";
|
|
40
|
+
import { createControllerIntegrationJobPort } from "./controller/jobClient.js";
|
|
31
41
|
import { runTaskWorkspaceCommand } from "./commands/taskWorkspaceCommands.js";
|
|
42
|
+
import { runWorkflowCommandAsync } from "./commands/workflowCommands.js";
|
|
43
|
+
import { createUpdatePorts } from "./cli/updatePorts.js";
|
|
44
|
+
import { createReleaseWorkflowPorts } from "./release/releaseWorkflowPorts.js";
|
|
45
|
+
import { readRuntimeIdentity } from "./release/runtimeRelease.js";
|
|
46
|
+
import { renderReleaseActivateResult, renderReleaseInstallResult, renderReleaseList, runReleaseActivate, runReleaseInstall, runReleaseList } from "./commands/releaseCommands.js";
|
|
32
47
|
import { reconcileTaskRemoteBaselines } from "./commands/taskCompletionGate.js";
|
|
33
48
|
import { FileCompletionManager, resolveCliIdentity } from "./completion/fileCompletionManager.js";
|
|
34
49
|
import { assertFileTaskControllerStorageCompatible, ensureFileTaskController, FileTaskWorkflowRuntime, refreshRunningFileTaskControllerConfiguration, refreshRunningFileTaskControllerEnvironment, restartFileTaskController, stopFileTaskController } from "./controller/clientRuntime.js";
|
|
@@ -37,6 +52,7 @@ import { FileSchedulerStoreAdapter } from "./controller/fileSchedulerStoreAdapte
|
|
|
37
52
|
import { cleanControllerResource } from "./controller/resourceCleanupLinux.js";
|
|
38
53
|
import { scanControllerResourceInventory } from "./controller/resourceInventoryLinux.js";
|
|
39
54
|
import { runSessionNotifyCommand } from "./controller/sessionNotify.js";
|
|
55
|
+
import { openSchedulerTelemetry } from "./telemetry/telemetryWiring.js";
|
|
40
56
|
import { runClaudeLifecycleHookCommand } from "./controller/claudeLifecycleHook.js";
|
|
41
57
|
import { runCodexLifecycleHookCommand } from "./controller/codexLifecycleHook.js";
|
|
42
58
|
import { buildDoctorReport, renderDoctor, runDoctorCommand } from "./doctor/doctor.js";
|
|
@@ -45,6 +61,7 @@ import { FileRoleLaunchPlanner } from "./executor/fileRoleLaunchPlanner.js";
|
|
|
45
61
|
import { TaskWorkspaceCoordinator, WorkspaceCleanupBlockedError } from "./repository/taskWorkspaceCoordinator.js";
|
|
46
62
|
import { FileTaskWorkspacePreparer, ReviewRoundWorkspaceEvidenceError } from "./repository/taskWorkspacePreparer.js";
|
|
47
63
|
import { inspectStorageSchema } from "./storage/storageSchema.js";
|
|
64
|
+
import { collectRuntimeBuildIdentity, collectStorageIdentity, countDroppedInboxEvents, createProductionRuntimeIdentityPorts, evaluateStorageHealth, resolveStatusIdentityEnabled } from "./observability/runtimeIdentity.js";
|
|
48
65
|
import { resolveYuiHome } from "./storage/taskStore.js";
|
|
49
66
|
import { openCompatibleFileTaskStore, validateCompatibleFileTaskStore } from "./storage/compatibleTaskStore.js";
|
|
50
67
|
import { resolveTaskRecordReference } from "./task/taskRecordReference.js";
|
|
@@ -82,7 +99,7 @@ void main().catch((error) => {
|
|
|
82
99
|
process.exitCode = 5;
|
|
83
100
|
});
|
|
84
101
|
export async function main() {
|
|
85
|
-
const taskFinalReviewContract = await preflightManagedTaskControlPlane();
|
|
102
|
+
const { contract: taskFinalReviewContract, verifiedStore } = await preflightManagedTaskControlPlane();
|
|
86
103
|
if (args.length === 0) {
|
|
87
104
|
emit(renderCommandHelp((await import("./cli/commandCatalog.js")).ROOT_COMMAND, VERSION));
|
|
88
105
|
return;
|
|
@@ -113,6 +130,37 @@ export async function main() {
|
|
|
113
130
|
return;
|
|
114
131
|
}
|
|
115
132
|
const home = resolveYuiHome(process.env);
|
|
133
|
+
if (args[0] === "release") {
|
|
134
|
+
const subcommand = args[1];
|
|
135
|
+
if (subcommand === "install" && args.length === 3) {
|
|
136
|
+
const result = runReleaseInstall(home, args[2]);
|
|
137
|
+
emit(renderReleaseInstallResult(result), false, result);
|
|
138
|
+
if (result.outcome === "aborted")
|
|
139
|
+
process.exitCode = 5;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (subcommand === "list" && args.length === 2) {
|
|
143
|
+
const result = runReleaseList(home);
|
|
144
|
+
emit(renderReleaseList(result), false, result);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (subcommand === "activate" && (args.length === 2 || args.length === 3)) {
|
|
148
|
+
const releaseId = args.length === 3
|
|
149
|
+
? args[2]
|
|
150
|
+
: runReleaseList(home).active ?? undefined;
|
|
151
|
+
if (releaseId === undefined) {
|
|
152
|
+
throw usageError("Release activate usage: yui release activate <release-id> "
|
|
153
|
+
+ "(or activate the active release when exactly one is installed).");
|
|
154
|
+
}
|
|
155
|
+
const result = await runReleaseActivate(home, releaseId);
|
|
156
|
+
emit(renderReleaseActivateResult(result), false, result);
|
|
157
|
+
if (result.outcome === "aborted" || result.outcome === "dual-owner") {
|
|
158
|
+
process.exitCode = 5;
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
throw usageError("Release usage: yui release install <source-dir> | list | activate [release-id].");
|
|
163
|
+
}
|
|
116
164
|
if (args[0] === "completion") {
|
|
117
165
|
await completionCommand(home, invocation.node);
|
|
118
166
|
return;
|
|
@@ -154,6 +202,17 @@ export async function main() {
|
|
|
154
202
|
emit(renderDoctor(report.checks, report.review));
|
|
155
203
|
return;
|
|
156
204
|
}
|
|
205
|
+
if (args[0] === "execution") {
|
|
206
|
+
// Issue 11 read-only audit: opens the Home read-only, never writes state,
|
|
207
|
+
// never wakes a Leader.
|
|
208
|
+
if (args[1] !== "audit") {
|
|
209
|
+
throw usageError("Execution usage: yui execution audit [--task <id>] [--since <iso>] [--until <iso>].");
|
|
210
|
+
}
|
|
211
|
+
const options = parseExecutionAuditOptions(args.slice(2));
|
|
212
|
+
const result = runExecutionAuditCommand(home, options);
|
|
213
|
+
emit(result.output, false, result.report);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
157
216
|
if (args[0] === "upgrade") {
|
|
158
217
|
// Mirror doctor/controller: needs a Home but self-manages the schema check,
|
|
159
218
|
// because upgrade must run against a non-current Home.
|
|
@@ -179,12 +238,50 @@ export async function main() {
|
|
|
179
238
|
}
|
|
180
239
|
throw usageError("Internal lifecycle callback usage is invalid.");
|
|
181
240
|
}
|
|
241
|
+
if (args[0] === "session") {
|
|
242
|
+
if (args[1] !== "reconcile") {
|
|
243
|
+
throw usageError("Session usage: yui session reconcile [--report] [--cleanup].");
|
|
244
|
+
}
|
|
245
|
+
const options = parseSessionReconcileOptions(args.slice(2));
|
|
246
|
+
const store = openCompatibleFileTaskStore(home);
|
|
247
|
+
const tmux = new TmuxManager(process.env.YUI_TMUX_BIN ?? "tmux", new NodeCommandExecutor(), { yuiHome: home });
|
|
248
|
+
const reconciliation = new SessionOwnerReconciliation({
|
|
249
|
+
home,
|
|
250
|
+
store,
|
|
251
|
+
environment: process.env,
|
|
252
|
+
tmux
|
|
253
|
+
});
|
|
254
|
+
const result = await runSessionReconcileCommand({
|
|
255
|
+
reconciliation,
|
|
256
|
+
options,
|
|
257
|
+
environment: process.env
|
|
258
|
+
});
|
|
259
|
+
process.exitCode = result.exitCode;
|
|
260
|
+
emit(result.output, false, result.data);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
182
263
|
if (args[0] === "controller") {
|
|
183
264
|
const method = args[1];
|
|
184
265
|
if (method === "identity" && args.length === 2) {
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
266
|
+
// Issue 02: the stable, read-only runtime identity receipt. It survives
|
|
267
|
+
// a Controller stop and answers build ID, package digest, backend, and
|
|
268
|
+
// worker state without a socket round-trip. When no receipt exists yet
|
|
269
|
+
// (a Controller that predates this feature), fall back to the
|
|
270
|
+
// authenticated socket identity so the command stays useful during
|
|
271
|
+
// rollout step 1.
|
|
272
|
+
let receipt = null;
|
|
273
|
+
try {
|
|
274
|
+
receipt = readRuntimeIdentity(home);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
// A corrupt or stale receipt (for example one written by an older
|
|
278
|
+
// Controller that predates the launch-identity fields) falls back to
|
|
279
|
+
// the live socket identity, which always carries the exact argv.
|
|
280
|
+
}
|
|
281
|
+
if (receipt !== null) {
|
|
282
|
+
emit("", false, receipt);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
188
285
|
try {
|
|
189
286
|
const identity = await callController(home, "controller.identity", {});
|
|
190
287
|
emit("", false, identity);
|
|
@@ -218,6 +315,39 @@ export async function main() {
|
|
|
218
315
|
scope: options.scope,
|
|
219
316
|
environment: process.env
|
|
220
317
|
});
|
|
318
|
+
if (resolveStatusIdentityEnabled(process.env)) {
|
|
319
|
+
// Issue 11 read-only identity/metrics section. Every fact is observed;
|
|
320
|
+
// missing producers render `unsupported` and storage contradictions
|
|
321
|
+
// fail closed with exit code 5.
|
|
322
|
+
const cliEntry = fileURLToPath(import.meta.url);
|
|
323
|
+
const packageRoot = resolve(cliEntry, "..", "..");
|
|
324
|
+
const build = collectRuntimeBuildIdentity(createProductionRuntimeIdentityPorts(packageRoot, cliEntry, process.env));
|
|
325
|
+
const storage = collectStorageIdentity(home);
|
|
326
|
+
const droppedEvents = countDroppedInboxEvents(home);
|
|
327
|
+
let runtime;
|
|
328
|
+
try {
|
|
329
|
+
const result = await callController(home, "controller.status", {}, { timeoutMs: 2_000 });
|
|
330
|
+
runtime = parseControllerRuntimeSnapshot(result, droppedEvents);
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
runtime = { source: "unsupported", droppedEvents };
|
|
334
|
+
}
|
|
335
|
+
const mismatch = summarizeDurablePhysicalMismatch(snapshot);
|
|
336
|
+
const identitySection = renderRuntimeIdentitySection({
|
|
337
|
+
build,
|
|
338
|
+
storage,
|
|
339
|
+
runtime,
|
|
340
|
+
mismatch,
|
|
341
|
+
inventoryRssBytes: snapshot.summary.rssBytes
|
|
342
|
+
});
|
|
343
|
+
emit(`${renderControllerResourceStatus(snapshot, options.verbose)}\n\n${identitySection}`, false, { ...snapshot, identity: { build, storage, runtime, mismatch } });
|
|
344
|
+
// Exit 5 only on hard contradictions (fail). A needs-repair state
|
|
345
|
+
// (pseudo-layout-7) is degraded but still readable via the file store,
|
|
346
|
+
// so it exits 0 with a DEGRADED health line and a precise repair action.
|
|
347
|
+
if (evaluateStorageHealth(storage).status === "fail")
|
|
348
|
+
process.exitCode = 5;
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
221
351
|
emit(renderControllerResourceStatus(snapshot, options.verbose), false, snapshot);
|
|
222
352
|
return;
|
|
223
353
|
}
|
|
@@ -270,8 +400,22 @@ export async function main() {
|
|
|
270
400
|
emit(renderControllerResult(controllerMethod, result), false, controllerMethod === "restart" ? result : undefined);
|
|
271
401
|
return;
|
|
272
402
|
}
|
|
403
|
+
if (args[0] === "resources") {
|
|
404
|
+
await assertFileTaskControllerStorageCompatible(home);
|
|
405
|
+
const resourcesStore = openCompatibleFileTaskStore(home);
|
|
406
|
+
const result = await runResourcesCommand(args.slice(1), resourcesStore);
|
|
407
|
+
emit(result.output, false, result.data);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
273
410
|
await assertFileTaskControllerStorageCompatible(home);
|
|
274
|
-
|
|
411
|
+
// Reuse the store the exact runtime preflight already opened and read for
|
|
412
|
+
// this same Home. Opening a second store would parse the unchanged large
|
|
413
|
+
// state a second time; the per-instance fingerprint cache still invalidates
|
|
414
|
+
// on an external writer, and the storage lock + revision CAS are unchanged.
|
|
415
|
+
const store = verifiedStore !== undefined
|
|
416
|
+
&& resolve(verifiedStore.rootDirectory()) === resolve(home)
|
|
417
|
+
? verifiedStore
|
|
418
|
+
: openCompatibleFileTaskStore(home);
|
|
275
419
|
const catalogs = new AgentConfigurationCatalogService(home, {
|
|
276
420
|
environment: process.env
|
|
277
421
|
});
|
|
@@ -287,7 +431,7 @@ export async function main() {
|
|
|
287
431
|
terminalInput: process.stdin,
|
|
288
432
|
onWarning: (message) => process.stderr.write(`Warning: ${message}\n`)
|
|
289
433
|
});
|
|
290
|
-
const schedulerStore = new FileSchedulerStoreAdapter(store);
|
|
434
|
+
const schedulerStore = new FileSchedulerStoreAdapter(store, openSchedulerTelemetry(home, process.env));
|
|
291
435
|
const planner = new FileRoleLaunchPlanner(home, store, { environment: process.env });
|
|
292
436
|
const workspacePreparer = new FileTaskWorkspacePreparer(home, store);
|
|
293
437
|
const runtime = new FileTaskWorkflowRuntime(home, store, schedulerStore, planner, tmux, workspacePreparer, {
|
|
@@ -443,7 +587,37 @@ export async function main() {
|
|
|
443
587
|
}
|
|
444
588
|
if (resolved[0] === "task") {
|
|
445
589
|
if (resolved[1] === "integration") {
|
|
446
|
-
const result = await runTaskIntegrationCommand(resolved.slice(2), store, home, {
|
|
590
|
+
const result = await runTaskIntegrationCommand(resolved.slice(2), store, home, {
|
|
591
|
+
environment: process.env,
|
|
592
|
+
jobPort: createControllerIntegrationJobPort(home, { environment: process.env, store })
|
|
593
|
+
});
|
|
594
|
+
emit(result.output, false, result.data);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if (resolved[1] === "change-set") {
|
|
598
|
+
const result = await runTaskChangeSetCommand(resolved.slice(2), store);
|
|
599
|
+
emit(result.output, false, result.data);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (resolved[1] === "overlap") {
|
|
603
|
+
const result = await runTaskOverlapCommand(resolved.slice(2), store);
|
|
604
|
+
emit(result.output, false, result.data);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (resolved[1] === "workflow"
|
|
608
|
+
&& (resolved[2] === "run" || resolved[2] === "resume")) {
|
|
609
|
+
const result = await runWorkflowCommandAsync(resolved.slice(2), store, {
|
|
610
|
+
environment: process.env,
|
|
611
|
+
yuiHome: home,
|
|
612
|
+
ports: createReleaseWorkflowPorts({
|
|
613
|
+
home,
|
|
614
|
+
updatePorts: createUpdatePorts(process.env),
|
|
615
|
+
projectStore: store
|
|
616
|
+
})
|
|
617
|
+
});
|
|
618
|
+
if (result.kind !== "output") {
|
|
619
|
+
throw new Error(`Task workflow ${resolved[2]} returned an invalid control result.`);
|
|
620
|
+
}
|
|
447
621
|
emit(result.output, false, result.data);
|
|
448
622
|
return;
|
|
449
623
|
}
|
|
@@ -669,7 +843,12 @@ export async function main() {
|
|
|
669
843
|
// transaction once its exact Lane ids exist.
|
|
670
844
|
}
|
|
671
845
|
}
|
|
672
|
-
let executionLaneWorkspaces
|
|
846
|
+
let executionLaneWorkspaces;
|
|
847
|
+
// Held only for a new Group's dispatch: the per-Project maintenance fence
|
|
848
|
+
// spans Lane preparation and the adoption transaction, and projectPaths is
|
|
849
|
+
// the under-fence snapshot the adoption CAS revalidates.
|
|
850
|
+
let laneDispatchRelease;
|
|
851
|
+
let laneDispatchProjectPaths;
|
|
673
852
|
let workItemIntegrationProof;
|
|
674
853
|
if (resolved[1] === "work" && resolved[2] === "accept") {
|
|
675
854
|
const workItemId = resolved[3];
|
|
@@ -695,12 +874,18 @@ export async function main() {
|
|
|
695
874
|
: { taskFinalReviewContract })
|
|
696
875
|
});
|
|
697
876
|
if (!completion.completed && !completion.activeTaskReview) {
|
|
698
|
-
await reconcileTaskRemoteBaselines(resolved[2], store, home, { environment: process.env });
|
|
877
|
+
await reconcileTaskRemoteBaselines(resolved[2], store, home, { environment: process.env, jobPort: createControllerIntegrationJobPort(home, { environment: process.env, store }) });
|
|
699
878
|
}
|
|
700
879
|
}
|
|
701
880
|
let candidateMaterialization;
|
|
702
881
|
let candidateMaterializationCommitted = false;
|
|
703
882
|
try {
|
|
883
|
+
const preparedLanes = await prepareExecutionLaneWorkspacesForCommand(resolved, store, workspacePreparer, process.env);
|
|
884
|
+
if (preparedLanes !== undefined) {
|
|
885
|
+
executionLaneWorkspaces = preparedLanes.workspaces;
|
|
886
|
+
laneDispatchRelease = preparedLanes.release;
|
|
887
|
+
laneDispatchProjectPaths = preparedLanes.projectPaths;
|
|
888
|
+
}
|
|
704
889
|
candidateMaterialization = await candidateMaterializationForTaskCommand(resolved, store, workspacePreparer, process.env, taskFinalReviewContract);
|
|
705
890
|
const candidateGitSnapshot = candidateMaterialization === undefined
|
|
706
891
|
? await candidateSnapshotForTaskCommand(resolved, store, workspacePreparer, process.env, taskFinalReviewContract)
|
|
@@ -726,6 +911,7 @@ export async function main() {
|
|
|
726
911
|
? {}
|
|
727
912
|
: { candidateWorkspace: candidateMaterialization.workspace ?? null }),
|
|
728
913
|
...(executionLaneWorkspaces === undefined ? {} : { executionLaneWorkspaces }),
|
|
914
|
+
...(laneDispatchProjectPaths === undefined ? {} : { laneDispatchProjectPaths }),
|
|
729
915
|
...(directTaskMainSnapshot === undefined ? {} : { directTaskMainSnapshot }),
|
|
730
916
|
...(actualTaskReviewCandidate === undefined
|
|
731
917
|
? {}
|
|
@@ -734,6 +920,13 @@ export async function main() {
|
|
|
734
920
|
...(laneSnapshotPreflight === undefined ? {} : { executionLaneGitSnapshot: laneSnapshotPreflight }),
|
|
735
921
|
...(taskRetirementProof === undefined ? {} : { taskRetirementProof })
|
|
736
922
|
});
|
|
923
|
+
// The dispatch transaction has now adopted (or rejected) the prepared
|
|
924
|
+
// Lane workspaces. Release the held fence so later output/review
|
|
925
|
+
// handling can take the per-Project fence itself.
|
|
926
|
+
if (laneDispatchRelease !== undefined) {
|
|
927
|
+
laneDispatchRelease();
|
|
928
|
+
laneDispatchRelease = undefined;
|
|
929
|
+
}
|
|
737
930
|
// The command transaction has now durably submitted the Candidate. Any
|
|
738
931
|
// later output/review handling must not roll back its Git snapshot.
|
|
739
932
|
candidateMaterializationCommitted = candidateMaterialization !== undefined;
|
|
@@ -867,6 +1060,10 @@ export async function main() {
|
|
|
867
1060
|
if (!candidateMaterializationCommitted) {
|
|
868
1061
|
await workspacePreparer.discardUnadoptedExecutionLaneWorkspaces(executionLaneWorkspaces);
|
|
869
1062
|
}
|
|
1063
|
+
if (laneDispatchRelease !== undefined) {
|
|
1064
|
+
laneDispatchRelease();
|
|
1065
|
+
laneDispatchRelease = undefined;
|
|
1066
|
+
}
|
|
870
1067
|
throw error;
|
|
871
1068
|
}
|
|
872
1069
|
}
|
|
@@ -874,6 +1071,24 @@ export async function main() {
|
|
|
874
1071
|
emit(runJobCommand(resolved.slice(1), store, { runtime }));
|
|
875
1072
|
return;
|
|
876
1073
|
}
|
|
1074
|
+
if (resolved[0] === "job") {
|
|
1075
|
+
emit(await runDurableJobCommand(resolved.slice(1), {
|
|
1076
|
+
home,
|
|
1077
|
+
json: jsonOutput,
|
|
1078
|
+
environment: process.env,
|
|
1079
|
+
store
|
|
1080
|
+
}));
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
if (resolved[0] === "telemetry") {
|
|
1084
|
+
emit(await runTelemetryCommand(resolved.slice(1), {
|
|
1085
|
+
home,
|
|
1086
|
+
json: jsonOutput,
|
|
1087
|
+
environment: process.env,
|
|
1088
|
+
store
|
|
1089
|
+
}));
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
877
1092
|
throw usageError(`Command is not connected to the restored FileTaskStore framework yet: ${resolved[0]}.`, renderCommandHelp(invocation.node, VERSION));
|
|
878
1093
|
}
|
|
879
1094
|
async function preflightManagedTaskControlPlane() {
|
|
@@ -896,7 +1111,7 @@ async function preflightManagedTaskControlPlane() {
|
|
|
896
1111
|
if (taskFinalReviewInvocation.request !== undefined) {
|
|
897
1112
|
throw new Error("Task final-review contract requires a verified exact Task control-plane invocation.");
|
|
898
1113
|
}
|
|
899
|
-
return undefined;
|
|
1114
|
+
return { contract: undefined, verifiedStore: undefined };
|
|
900
1115
|
}
|
|
901
1116
|
if (process.env.YUI_SESSION_SCOPE !== "task") {
|
|
902
1117
|
throw new Error("Exact Task control-plane invocation requires a managed Task runtime.");
|
|
@@ -926,23 +1141,27 @@ async function preflightManagedTaskControlPlane() {
|
|
|
926
1141
|
const preallocatedClaudeCallback = args.length === 2
|
|
927
1142
|
&& args[0] === "internal"
|
|
928
1143
|
&& args[1] === "claude-hook";
|
|
929
|
-
|
|
1144
|
+
const verifiedStore = openCompatibleFileTaskStore(control.yuiHome);
|
|
1145
|
+
assertExactTaskRuntimeState(runtime, verifiedStore, preallocatedClaudeCallback
|
|
930
1146
|
? { preallocatedNativeSessionReservation: { yuiHome: control.yuiHome } }
|
|
931
1147
|
: {});
|
|
932
1148
|
const request = taskFinalReviewInvocation.request;
|
|
933
1149
|
if (request === undefined)
|
|
934
|
-
return undefined;
|
|
1150
|
+
return { contract: undefined, verifiedStore };
|
|
935
1151
|
if (runtime.roleName !== "leader") {
|
|
936
1152
|
throw new Error("Only the exact Task Leader invocation may establish a final-review contract.");
|
|
937
1153
|
}
|
|
938
1154
|
if (request.taskId !== runtime.taskId) {
|
|
939
1155
|
throw new Error(`Task final-review contract Task id mismatch: expected ${runtime.taskId}, found ${request.taskId}.`);
|
|
940
1156
|
}
|
|
941
|
-
return
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1157
|
+
return {
|
|
1158
|
+
contract: createTaskFinalReviewContract({
|
|
1159
|
+
taskId: runtime.taskId,
|
|
1160
|
+
reviewerRoleName: request.reviewerRoleName,
|
|
1161
|
+
controlPlaneDigest: digest
|
|
1162
|
+
}),
|
|
1163
|
+
verifiedStore
|
|
1164
|
+
};
|
|
946
1165
|
}
|
|
947
1166
|
function cleanupCliError(error, fallbackResource) {
|
|
948
1167
|
if (error instanceof WorkspaceCleanupBlockedError) {
|
|
@@ -1145,20 +1364,45 @@ async function prepareExecutionLaneWorkspacesForCommand(args, store, preparer, e
|
|
|
1145
1364
|
return undefined;
|
|
1146
1365
|
const groupId = group?.id ?? `execution-group-${store.peekNextAgentRunId(item.taskId)}`;
|
|
1147
1366
|
const laneIds = plan.laneIds;
|
|
1367
|
+
// A new Group's Lanes are not yet durable, so their worktrees would be
|
|
1368
|
+
// unadopted between preparation and the dispatch transaction. Hold ONE
|
|
1369
|
+
// per-Project maintenance fence across both, so a project migrate cannot
|
|
1370
|
+
// switch the catalog in that gap and strand a Lane on the external
|
|
1371
|
+
// checkout. An existing Group's Lanes are adopted inside their own fence,
|
|
1372
|
+
// so no outer fence is held.
|
|
1373
|
+
const held = group === undefined
|
|
1374
|
+
? preparer.acquireTaskProjectMaintenanceLocks(item.taskId)
|
|
1375
|
+
: undefined;
|
|
1148
1376
|
const map = new Map();
|
|
1149
1377
|
try {
|
|
1378
|
+
let projectPaths;
|
|
1379
|
+
if (held !== undefined) {
|
|
1380
|
+
const paths = new Map();
|
|
1381
|
+
for (const { projectId } of held.current.projectBindings) {
|
|
1382
|
+
const project = store.getProject(projectId);
|
|
1383
|
+
if (project === null)
|
|
1384
|
+
throw new Error(`Project not found: ${projectId}.`);
|
|
1385
|
+
paths.set(projectId, project.path);
|
|
1386
|
+
}
|
|
1387
|
+
projectPaths = paths;
|
|
1388
|
+
}
|
|
1150
1389
|
for (const laneId of laneIds.filter((value) => value.length > 0)) {
|
|
1151
1390
|
map.set(laneId, await preparer.prepareExecutionLaneWorkspace(item.taskId, groupId, laneId, {
|
|
1152
1391
|
purpose: "execution",
|
|
1153
1392
|
workItemId: item.id
|
|
1154
|
-
}));
|
|
1393
|
+
}, held === undefined ? undefined : { current: held.current }));
|
|
1155
1394
|
}
|
|
1395
|
+
return { workspaces: map, release: held?.release, projectPaths };
|
|
1156
1396
|
}
|
|
1157
1397
|
catch (error) {
|
|
1398
|
+
// Compensate (discard unadopted Lane worktrees) BEFORE releasing the
|
|
1399
|
+
// fence: a concurrent project migrate must not switch the catalog while
|
|
1400
|
+
// external-backed worktrees are still identifiable for removal.
|
|
1158
1401
|
await preparer.discardUnadoptedExecutionLaneWorkspaces(map);
|
|
1402
|
+
if (held !== undefined)
|
|
1403
|
+
held.release();
|
|
1159
1404
|
throw error;
|
|
1160
1405
|
}
|
|
1161
|
-
return map;
|
|
1162
1406
|
}
|
|
1163
1407
|
async function prepareReviewLaneWorkspaces(taskId, reviewRoundId, store, preparer) {
|
|
1164
1408
|
const round = store.getReviewRound(taskId, reviewRoundId);
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { usageError } from "../errors/cliError.js";
|
|
2
|
-
import { DEFAULT_RECONCILIATION_INTERVAL_SECONDS, reconciliationIntervalMilliseconds } from "../config/yuiConfig.js";
|
|
2
|
+
import { DEFAULT_RECONCILIATION_INTERVAL_SECONDS, reconciliationIntervalMilliseconds, resolveResourcesGcAutoQuarantine, resolveResourcesGcMode } from "../config/yuiConfig.js";
|
|
3
3
|
import { resolveTimeZone } from "../output/timePresentation.js";
|
|
4
|
-
import { REVIEW_TRIGGERS } from "../review/reviewConfig.js";
|
|
4
|
+
import { REVIEW_FINDING_LEDGER_MODES, REVIEW_TRIGGERS } from "../review/reviewConfig.js";
|
|
5
|
+
import { DEFAULT_LEADER_NEXT_ACTION_MODE, LEADER_NEXT_ACTION_MODES, resolveLeaderNextActionMode } from "../config/yuiConfig.js";
|
|
5
6
|
const CONFIG_SET_USAGE = "Config set usage: yui config set "
|
|
6
7
|
+ "<--time-zone <IANA timezone> | "
|
|
7
|
-
+ "--reconciliation-interval-seconds <seconds
|
|
8
|
+
+ "--reconciliation-interval-seconds <seconds> | "
|
|
9
|
+
+ "--resources-gc-mode <report|quarantine> | "
|
|
10
|
+
+ "--resources-gc-auto-quarantine <true|false>>.";
|
|
8
11
|
export function runConfigCommand(args, store) {
|
|
9
12
|
const [command, ...rest] = args;
|
|
10
13
|
if (command === "review")
|
|
11
14
|
return runReviewConfigCommand(rest, store);
|
|
15
|
+
if (command === "leader-next-action")
|
|
16
|
+
return runLeaderNextActionConfigCommand(rest, store);
|
|
12
17
|
if (command === "show") {
|
|
13
18
|
if (rest.length !== 0)
|
|
14
19
|
throw usageError("Config show usage: yui config show.");
|
|
@@ -19,6 +24,9 @@ export function runConfigCommand(args, store) {
|
|
|
19
24
|
return [
|
|
20
25
|
`Time zone: ${resolveTimeZone(config.timeZone)}`,
|
|
21
26
|
`Reconciliation interval: ${reconciliationIntervalSeconds} seconds`,
|
|
27
|
+
`Leader next-action mode: ${resolveLeaderNextActionMode(config.leaderNextActionMode)}`,
|
|
28
|
+
`Resources GC mode: ${resolveResourcesGcMode(config.resourcesGcMode)}`,
|
|
29
|
+
`Resources GC auto-quarantine: ${resolveResourcesGcAutoQuarantine(config.resourcesGcAutoQuarantine) ? "on" : "off"}`,
|
|
22
30
|
""
|
|
23
31
|
].join("\n");
|
|
24
32
|
}
|
|
@@ -41,12 +49,65 @@ export function runConfigCommand(args, store) {
|
|
|
41
49
|
});
|
|
42
50
|
return `Reconciliation interval set to ${reconciliationIntervalSeconds} seconds\n`;
|
|
43
51
|
}
|
|
52
|
+
if (rest[0] === "--resources-gc-mode") {
|
|
53
|
+
const resourcesGcMode = validatedConfigValue(() => resolveResourcesGcMode(rest[1]));
|
|
54
|
+
store.transaction((tx) => {
|
|
55
|
+
tx.saveConfig({ ...tx.getConfig(), resourcesGcMode });
|
|
56
|
+
});
|
|
57
|
+
return `Resources GC mode set to ${resourcesGcMode}\n`;
|
|
58
|
+
}
|
|
59
|
+
if (rest[0] === "--resources-gc-auto-quarantine") {
|
|
60
|
+
const resourcesGcAutoQuarantine = validatedConfigValue(() => resolveResourcesGcAutoQuarantine(rest[1] === "true" ? true : rest[1] === "false" ? false : rest[1]));
|
|
61
|
+
store.transaction((tx) => {
|
|
62
|
+
tx.saveConfig({ ...tx.getConfig(), resourcesGcAutoQuarantine });
|
|
63
|
+
});
|
|
64
|
+
return `Resources GC auto-quarantine set to ${resourcesGcAutoQuarantine ? "on" : "off"}\n`;
|
|
65
|
+
}
|
|
44
66
|
throw configSetUsageError();
|
|
45
67
|
}
|
|
46
68
|
throw usageError(command === undefined
|
|
47
69
|
? "Config command is required."
|
|
48
70
|
: `Unknown command: config ${command}`);
|
|
49
71
|
}
|
|
72
|
+
function runLeaderNextActionConfigCommand(args, store) {
|
|
73
|
+
const [command, ...rest] = args;
|
|
74
|
+
if (command === "show") {
|
|
75
|
+
if (rest.length !== 0) {
|
|
76
|
+
throw usageError("Config leader-next-action show usage: yui config leader-next-action show.");
|
|
77
|
+
}
|
|
78
|
+
return `Leader next-action mode: ${resolveLeaderNextActionMode(store.getConfig().leaderNextActionMode)}\n`;
|
|
79
|
+
}
|
|
80
|
+
if (command === "set") {
|
|
81
|
+
const usage = "Config leader-next-action set usage: "
|
|
82
|
+
+ `yui config leader-next-action set <${LEADER_NEXT_ACTION_MODES.join("|")}>.`;
|
|
83
|
+
if (rest.length !== 1)
|
|
84
|
+
throw usageError(usage);
|
|
85
|
+
let mode;
|
|
86
|
+
try {
|
|
87
|
+
mode = resolveLeaderNextActionMode(rest[0]);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
throw usageError(error instanceof Error ? error.message : String(error), usage);
|
|
91
|
+
}
|
|
92
|
+
store.transaction((tx) => {
|
|
93
|
+
tx.saveConfig({ ...tx.getConfig(), leaderNextActionMode: mode });
|
|
94
|
+
});
|
|
95
|
+
return `Leader next-action mode set to ${mode}\n`;
|
|
96
|
+
}
|
|
97
|
+
if (command === "clear") {
|
|
98
|
+
if (rest.length !== 0) {
|
|
99
|
+
throw usageError("Config leader-next-action clear usage: yui config leader-next-action clear.");
|
|
100
|
+
}
|
|
101
|
+
store.transaction((tx) => {
|
|
102
|
+
const { leaderNextActionMode: _mode, ...config } = tx.getConfig();
|
|
103
|
+
tx.saveConfig(config);
|
|
104
|
+
});
|
|
105
|
+
return `Leader next-action mode reset to ${DEFAULT_LEADER_NEXT_ACTION_MODE}\n`;
|
|
106
|
+
}
|
|
107
|
+
throw usageError(command === undefined
|
|
108
|
+
? "Config leader-next-action command is required."
|
|
109
|
+
: `Unknown command: config leader-next-action ${command}`);
|
|
110
|
+
}
|
|
50
111
|
function runReviewConfigCommand(args, store) {
|
|
51
112
|
const [command, ...rest] = args;
|
|
52
113
|
if (command === "show") {
|
|
@@ -55,7 +116,7 @@ function runReviewConfigCommand(args, store) {
|
|
|
55
116
|
const review = store.getConfig().review;
|
|
56
117
|
return review === undefined
|
|
57
118
|
? "Review: disabled\n"
|
|
58
|
-
: `Review: ${review.roleName} (${review.trigger})\n`;
|
|
119
|
+
: `Review: ${review.roleName} (${review.trigger}; finding ledger: ${review.findingLedger ?? "shadow"})\n`;
|
|
59
120
|
}
|
|
60
121
|
if (command === "clear") {
|
|
61
122
|
if (rest.length !== 0)
|
|
@@ -68,14 +129,15 @@ function runReviewConfigCommand(args, store) {
|
|
|
68
129
|
}
|
|
69
130
|
if (command === "set") {
|
|
70
131
|
const usage = "Config review set usage: "
|
|
71
|
-
+ "yui config review set --role <global-role> --trigger <always|leader|final
|
|
72
|
-
|
|
132
|
+
+ "yui config review set --role <global-role> --trigger <always|leader|final> "
|
|
133
|
+
+ "[--finding-ledger <shadow|enforce>].";
|
|
134
|
+
if (rest.length !== 4 && rest.length !== 6)
|
|
73
135
|
throw usageError(usage);
|
|
74
136
|
const options = new Map();
|
|
75
137
|
for (let index = 0; index < rest.length; index += 2) {
|
|
76
138
|
const name = rest[index];
|
|
77
139
|
const value = rest[index + 1];
|
|
78
|
-
if (!["--role", "--trigger"].includes(name)
|
|
140
|
+
if (!["--role", "--trigger", "--finding-ledger"].includes(name)
|
|
79
141
|
|| value === undefined
|
|
80
142
|
|| options.has(name)) {
|
|
81
143
|
throw usageError(usage);
|
|
@@ -93,13 +155,25 @@ function runReviewConfigCommand(args, store) {
|
|
|
93
155
|
throw usageError(`Global Role not found: ${roleName}.`);
|
|
94
156
|
}
|
|
95
157
|
const trigger = rawTrigger;
|
|
158
|
+
const rawLedgerMode = options.get("--finding-ledger")?.trim();
|
|
159
|
+
let findingLedger;
|
|
160
|
+
if (rawLedgerMode !== undefined) {
|
|
161
|
+
if (!REVIEW_FINDING_LEDGER_MODES.includes(rawLedgerMode)) {
|
|
162
|
+
throw usageError(usage);
|
|
163
|
+
}
|
|
164
|
+
findingLedger = rawLedgerMode;
|
|
165
|
+
}
|
|
96
166
|
store.transaction((tx) => {
|
|
97
167
|
tx.saveConfig({
|
|
98
168
|
...tx.getConfig(),
|
|
99
|
-
review: {
|
|
169
|
+
review: {
|
|
170
|
+
roleName,
|
|
171
|
+
trigger,
|
|
172
|
+
...(findingLedger === undefined ? {} : { findingLedger })
|
|
173
|
+
}
|
|
100
174
|
});
|
|
101
175
|
});
|
|
102
|
-
return `Review set to ${roleName} (${trigger})\n`;
|
|
176
|
+
return `Review set to ${roleName} (${trigger}; finding ledger: ${findingLedger ?? "shadow"})\n`;
|
|
103
177
|
}
|
|
104
178
|
throw usageError(command === undefined
|
|
105
179
|
? "Config review command is required."
|