@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { usageError } from "../errors/cliError.js";
|
|
2
2
|
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
3
3
|
import { DEFAULT_RECONCILIATION_INTERVAL_SECONDS } from "../config/yuiConfig.js";
|
|
4
|
+
import { evaluateStorageHealth, UNSUPPORTED } from "../observability/runtimeIdentity.js";
|
|
4
5
|
export function parseControllerStatusOptions(args) {
|
|
5
6
|
const allowed = new Set(["--all", "--verbose"]);
|
|
6
7
|
if (args.some((argument) => !allowed.has(argument))
|
|
@@ -122,6 +123,108 @@ export function renderControllerResourceStatus(snapshot, verbose, width = defaul
|
|
|
122
123
|
}
|
|
123
124
|
return lines.join("\n");
|
|
124
125
|
}
|
|
126
|
+
export function summarizeDurablePhysicalMismatch(snapshot) {
|
|
127
|
+
let durableActiveSessions = 0;
|
|
128
|
+
let residualProcesses = 0;
|
|
129
|
+
let liveResidualResources = 0;
|
|
130
|
+
let protectedWithoutProcess = 0;
|
|
131
|
+
for (const resource of snapshot.resources) {
|
|
132
|
+
if (resource.kind === "agent-session") {
|
|
133
|
+
if (resource.disposition === "protected") {
|
|
134
|
+
durableActiveSessions += 1;
|
|
135
|
+
if (resource.processes.length === 0)
|
|
136
|
+
protectedWithoutProcess += 1;
|
|
137
|
+
}
|
|
138
|
+
else if (resource.processes.length > 0) {
|
|
139
|
+
liveResidualResources += 1;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (resource.disposition !== "protected") {
|
|
143
|
+
residualProcesses += resource.processes.length;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
durableActiveSessions,
|
|
148
|
+
residualProcesses,
|
|
149
|
+
liveResidualResources,
|
|
150
|
+
protectedWithoutProcess
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
export function parseControllerRuntimeSnapshot(result, droppedEvents) {
|
|
154
|
+
if (result === null || typeof result !== "object" || Array.isArray(result)) {
|
|
155
|
+
return { source: "unsupported", droppedEvents };
|
|
156
|
+
}
|
|
157
|
+
const record = result;
|
|
158
|
+
const runtime = (record.runtime ?? {});
|
|
159
|
+
const inbox = runtime.inbox;
|
|
160
|
+
const drain = runtime.drain;
|
|
161
|
+
const commands = runtime.commands;
|
|
162
|
+
const routes = commands?.routes;
|
|
163
|
+
const dispatcher = commands?.dispatcher;
|
|
164
|
+
const eventLoopDelay = commands?.eventLoopDelay;
|
|
165
|
+
const queueDepth = (typeof routes?.inFlight === "number" ? routes.inFlight : 0)
|
|
166
|
+
+ (typeof dispatcher?.inFlight === "number" ? dispatcher.inFlight : 0);
|
|
167
|
+
return {
|
|
168
|
+
source: "controller.status",
|
|
169
|
+
pid: typeof record.pid === "number" ? record.pid : undefined,
|
|
170
|
+
uptimeMs: typeof record.uptimeMs === "number" ? record.uptimeMs : undefined,
|
|
171
|
+
rssBytes: typeof record.rssBytes === "number" ? record.rssBytes : undefined,
|
|
172
|
+
protocolVersion: typeof record.protocolVersion === "number"
|
|
173
|
+
? record.protocolVersion
|
|
174
|
+
: undefined,
|
|
175
|
+
inbox: inbox === undefined ? undefined : {
|
|
176
|
+
depth: typeof inbox.depth === "number" ? inbox.depth : 0,
|
|
177
|
+
semanticDepth: typeof inbox.semanticDepth === "number" ? inbox.semanticDepth : 0,
|
|
178
|
+
progressDepth: typeof inbox.progressDepth === "number" ? inbox.progressDepth : 0
|
|
179
|
+
},
|
|
180
|
+
coalescedEvents: typeof drain?.progressEventsCoalesced === "number"
|
|
181
|
+
? drain.progressEventsCoalesced
|
|
182
|
+
: undefined,
|
|
183
|
+
droppedEvents,
|
|
184
|
+
queueDepth,
|
|
185
|
+
oldestInFlightAgeMs: typeof routes?.oldestInFlightAgeMs === "number"
|
|
186
|
+
? routes.oldestInFlightAgeMs
|
|
187
|
+
: null,
|
|
188
|
+
eventLoopMaxLagMs: typeof eventLoopDelay?.maximumLagMs === "number"
|
|
189
|
+
? eventLoopDelay.maximumLagMs
|
|
190
|
+
: undefined
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
export function renderRuntimeIdentitySection(input) {
|
|
194
|
+
const { build, storage, runtime, mismatch, inventoryRssBytes } = input;
|
|
195
|
+
const health = evaluateStorageHealth(storage);
|
|
196
|
+
const lines = [
|
|
197
|
+
"Runtime identity",
|
|
198
|
+
` Package ${build.packageName} ${build.packageVersion}`,
|
|
199
|
+
` Package digest ${build.packageDigest}`,
|
|
200
|
+
` Entry ${build.entryPath}`,
|
|
201
|
+
` Entry digest ${build.entryDigest}`,
|
|
202
|
+
` Source commit ${build.sourceCommit}`,
|
|
203
|
+
` Node ${build.nodeVersion} (${build.platform})`,
|
|
204
|
+
` Storage layout ${storage.logicalLayout} (manifest ${storage.manifestStatus}) · backend ${storage.configuredBackend} · worker ${storage.workerEnabled ? "on" : "off"}`,
|
|
205
|
+
` Store files state.json ${storage.physicalStateJson.present ? "present" : "absent"} · yui.db ${storage.physicalDatabase.present ? "present" : "absent"}${storage.physicalDatabase.wal ? " +WAL" : ""}${storage.physicalDatabase.present && storage.physicalDatabase.health !== UNSUPPORTED ? ` (${storage.physicalDatabase.health})` : ""}`
|
|
206
|
+
];
|
|
207
|
+
for (const finding of storage.findings) {
|
|
208
|
+
lines.push(` ! ${finding.severity === "contradiction" ? "CONTRADICTION" : finding.severity === "needs-repair" ? "NEEDS-REPAIR" : "warning"} ${finding.code}: ${finding.message}`, ` remediation: ${finding.remediation}`);
|
|
209
|
+
}
|
|
210
|
+
lines.push(` Health ${health.status === "ok" ? "ok" : health.status === "degraded" ? "DEGRADED (needs-repair above)" : "FAIL (contradictions above)"}`, ` Controller ${runtime.source === "controller.status" && runtime.pid !== undefined
|
|
211
|
+
? `PID ${runtime.pid}${runtime.uptimeMs === undefined ? "" : `, up ${formatDuration(runtime.uptimeMs)}`}${runtime.protocolVersion === undefined ? "" : `, protocol ${runtime.protocolVersion}`}`
|
|
212
|
+
: "not running (unsupported)"}`, ` Controller RSS ${runtime.rssBytes !== undefined ? formatBytes(runtime.rssBytes) : UNSUPPORTED} (inventory ${formatBytes(inventoryRssBytes)})`, ` Command queue depth ${runtime.queueDepth ?? UNSUPPORTED} · oldest ${runtime.oldestInFlightAgeMs === undefined
|
|
213
|
+
? UNSUPPORTED
|
|
214
|
+
: runtime.oldestInFlightAgeMs === null
|
|
215
|
+
? "—"
|
|
216
|
+
: formatDuration(runtime.oldestInFlightAgeMs)}`, ` Runtime inbox ${runtime.inbox === undefined
|
|
217
|
+
? UNSUPPORTED
|
|
218
|
+
: `depth ${runtime.inbox.depth} (semantic ${runtime.inbox.semanticDepth}, progress ${runtime.inbox.progressDepth})`} · coalesced ${runtime.coalescedEvents ?? UNSUPPORTED} · dropped ${runtime.droppedEvents ?? UNSUPPORTED}`, ` Event loop ${runtime.eventLoopMaxLagMs === undefined
|
|
219
|
+
? UNSUPPORTED
|
|
220
|
+
: `max lag ${runtime.eventLoopMaxLagMs.toFixed(0)}ms`}`,
|
|
221
|
+
// Persistence latency/lock-wait have no producer in the current
|
|
222
|
+
// controller.status RPC; render the documented `unsupported` fallback
|
|
223
|
+
// rather than silently dropping the field. A future capability provider
|
|
224
|
+
// can register a persistence observer without changing this schema.
|
|
225
|
+
` Persistence latency ${UNSUPPORTED} · lock/busy wait ${UNSUPPORTED}`, ` Durable/physical ${mismatch.durableActiveSessions} active sessions · ${mismatch.residualProcesses} residual processes · ${mismatch.liveResidualResources} live residual · ${mismatch.protectedWithoutProcess} protected without process`);
|
|
226
|
+
return lines.join("\n");
|
|
227
|
+
}
|
|
125
228
|
export async function runInteractiveControllerCleanup(input) {
|
|
126
229
|
if (!input.io.interactive) {
|
|
127
230
|
throw usageError("Controller cleanup requires an interactive terminal.");
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { leaderNextActionMode } from "../config/yuiConfig.js";
|
|
2
|
+
import { usageError } from "../errors/cliError.js";
|
|
3
|
+
import { detectDeliveryDuplicates, evaluateDeliveryGuard, evaluateSemanticBudget, formatDeliveryDuplicate } from "../task/deliveryGuard.js";
|
|
4
|
+
export function runDeliveryGuardPreflight(store, taskId, intent, options = {}) {
|
|
5
|
+
const mode = leaderNextActionMode(store.getConfig().leaderNextActionMode, options.environment);
|
|
6
|
+
if (mode === "display")
|
|
7
|
+
return { warnings: [] };
|
|
8
|
+
const facts = store.readNextActionFacts(taskId);
|
|
9
|
+
if (facts === null)
|
|
10
|
+
return { warnings: [] };
|
|
11
|
+
const outcome = evaluateDeliveryGuard(detectDeliveryDuplicates(facts, intent), mode);
|
|
12
|
+
const warnings = outcome.warnings.map(formatDeliveryDuplicate);
|
|
13
|
+
if (outcome.blocked !== null) {
|
|
14
|
+
throw usageError(`${formatDeliveryDuplicate(outcome.blocked)} `
|
|
15
|
+
+ "Reuse the existing proof, or set leader.nextActionMode=warn to proceed with a warning.");
|
|
16
|
+
}
|
|
17
|
+
if (options.budget === true) {
|
|
18
|
+
const budget = evaluateSemanticBudget(facts);
|
|
19
|
+
if (budget.exhausted) {
|
|
20
|
+
warnings.push(`Semantic progress budget: ${budget.reason}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return { warnings };
|
|
24
|
+
}
|
|
25
|
+
/** Prepend guard warnings to a command's normal output. */
|
|
26
|
+
export function withGuardWarnings(preflight, output) {
|
|
27
|
+
if (preflight.warnings.length === 0)
|
|
28
|
+
return output;
|
|
29
|
+
return `${preflight.warnings.map((warning) => `Warning: ${warning}\n`).join("")}${output}`;
|
|
30
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { usageError } from "../errors/cliError.js";
|
|
2
|
+
import { ensureFileTaskController } from "../controller/clientRuntime.js";
|
|
3
|
+
import { acknowledgeDurableJob, cancelDurableJob, getDurableJob, startDurableJob } from "../controller/jobClient.js";
|
|
4
|
+
import { resolveJobCaller, taskActor } from "./taskActor.js";
|
|
5
|
+
/**
|
|
6
|
+
* The textual `--owner` forms accepted by `job start`. The public help text in
|
|
7
|
+
* the command catalog must document exactly these forms.
|
|
8
|
+
*/
|
|
9
|
+
export const JOB_OWNER_USAGE = "task|work-item:<id>|integration-attempt:<id>";
|
|
10
|
+
/**
|
|
11
|
+
* `yui job start|get|cancel` — the operational surface for Controller-owned
|
|
12
|
+
* DurableJobs. Every request goes through the Controller socket; this command
|
|
13
|
+
* never spawns a runner itself.
|
|
14
|
+
*/
|
|
15
|
+
export async function runDurableJobCommand(args, options) {
|
|
16
|
+
const [command, ...rest] = args;
|
|
17
|
+
if (command === "start")
|
|
18
|
+
return startJob(rest, options);
|
|
19
|
+
if (command === "get")
|
|
20
|
+
return getJob(rest, options);
|
|
21
|
+
if (command === "cancel")
|
|
22
|
+
return cancelJob(rest, options);
|
|
23
|
+
if (command === "acknowledge")
|
|
24
|
+
return acknowledgeJob(rest, options);
|
|
25
|
+
throw usageError(command === undefined
|
|
26
|
+
? "Job command is required: start, get, cancel, or acknowledge."
|
|
27
|
+
: `Unknown command: job ${command}`, "yui job start --task <id> --project <project> --head <sha> --workspace <dir> "
|
|
28
|
+
+ "--step <name>=<command> [--step ...] [--env K=V ...] "
|
|
29
|
+
+ `[--owner ${JOB_OWNER_USAGE}] [--retry-of <job-id>]\n`
|
|
30
|
+
+ "yui job get --task <id> --job <job-id>\n"
|
|
31
|
+
+ "yui job cancel --task <id> --job <job-id>\n"
|
|
32
|
+
+ "yui job acknowledge --task <id> --job <job-id>");
|
|
33
|
+
}
|
|
34
|
+
async function startJob(args, options) {
|
|
35
|
+
const parsed = parseStartArgs(args);
|
|
36
|
+
await ensureFileTaskController(options.home, { environment: options.environment });
|
|
37
|
+
// rr8/rr12: Bind the declared owner to the caller's managed identity. The
|
|
38
|
+
// Controller rejects a Reviewer, constrains a Worker to its own Work Item,
|
|
39
|
+
// and requires a verified Leader assertion for user scope.
|
|
40
|
+
const caller = resolveJobCaller(options.environment, parsed.taskId, options.store);
|
|
41
|
+
const params = {
|
|
42
|
+
taskId: parsed.taskId,
|
|
43
|
+
owner: parsed.owner,
|
|
44
|
+
projectId: parsed.projectId,
|
|
45
|
+
head: parsed.head,
|
|
46
|
+
workspace: parsed.workspace,
|
|
47
|
+
env: parsed.env,
|
|
48
|
+
steps: parsed.steps,
|
|
49
|
+
caller,
|
|
50
|
+
...(parsed.retryOf === undefined ? {} : { retryOf: parsed.retryOf })
|
|
51
|
+
};
|
|
52
|
+
const result = await startDurableJob(options.home, params);
|
|
53
|
+
if (options.json === true)
|
|
54
|
+
return `${JSON.stringify(result, null, 2)}\n`;
|
|
55
|
+
const verb = result.created ? "queued" : "already exists";
|
|
56
|
+
return `Job ${result.job.id} ${verb} (status: ${result.job.status})\n`;
|
|
57
|
+
}
|
|
58
|
+
async function getJob(args, options) {
|
|
59
|
+
const ref = parseRefArgs(args, "get");
|
|
60
|
+
await ensureFileTaskController(options.home, { environment: options.environment });
|
|
61
|
+
const job = await getDurableJob(options.home, ref.taskId, ref.jobId);
|
|
62
|
+
if (options.json === true)
|
|
63
|
+
return `${JSON.stringify(job, null, 2)}\n`;
|
|
64
|
+
const lines = [
|
|
65
|
+
`Job ${job.id} (task ${job.taskId})`,
|
|
66
|
+
` status: ${job.status}`,
|
|
67
|
+
` owner: ${formatOwner(job.owner)}`,
|
|
68
|
+
` head: ${job.head}`,
|
|
69
|
+
` workspace: ${job.workspace}`,
|
|
70
|
+
` created: ${job.createdAt}`
|
|
71
|
+
];
|
|
72
|
+
if (job.startedAt !== undefined)
|
|
73
|
+
lines.push(` started: ${job.startedAt}`);
|
|
74
|
+
if (job.terminalAt !== undefined)
|
|
75
|
+
lines.push(` terminal: ${job.terminalAt}`);
|
|
76
|
+
if (job.result !== undefined) {
|
|
77
|
+
lines.push(` outcome: ${job.result.outcome}`);
|
|
78
|
+
if (job.result.failedStep !== undefined)
|
|
79
|
+
lines.push(` failed step: ${job.result.failedStep}`);
|
|
80
|
+
if (job.result.unknownReason !== undefined)
|
|
81
|
+
lines.push(` reason: ${job.result.unknownReason}`);
|
|
82
|
+
}
|
|
83
|
+
for (const step of job.steps) {
|
|
84
|
+
lines.push(` step ${step.name}: ${step.command}`);
|
|
85
|
+
}
|
|
86
|
+
return `${lines.join("\n")}\n`;
|
|
87
|
+
}
|
|
88
|
+
async function cancelJob(args, options) {
|
|
89
|
+
const ref = parseRefArgs(args, "cancel");
|
|
90
|
+
await ensureFileTaskController(options.home, { environment: options.environment });
|
|
91
|
+
// rr8/rr12: Bind the cancel request to the caller's managed identity.
|
|
92
|
+
const caller = resolveJobCaller(options.environment, ref.taskId, options.store);
|
|
93
|
+
const result = await cancelDurableJob(options.home, ref.taskId, ref.jobId, caller);
|
|
94
|
+
if (options.json === true)
|
|
95
|
+
return `${JSON.stringify(result, null, 2)}\n`;
|
|
96
|
+
return result.cancelRequested
|
|
97
|
+
? `Cancel requested for job ${result.job.id}\n`
|
|
98
|
+
: `Job ${result.job.id} is already terminal (${result.job.status})\n`;
|
|
99
|
+
}
|
|
100
|
+
async function acknowledgeJob(args, options) {
|
|
101
|
+
const ref = parseRefArgs(args, "acknowledge");
|
|
102
|
+
// rr5/f5(a): only the Task Leader may acknowledge an
|
|
103
|
+
// unknown-needs-attention job. A non-Leader (Worker, Operator, or plain
|
|
104
|
+
// user) is rejected at the CLI boundary.
|
|
105
|
+
const actor = taskActor(options.environment, ref.taskId);
|
|
106
|
+
if (actor !== "leader") {
|
|
107
|
+
throw usageError("Only the Task Leader may acknowledge a DurableJob: "
|
|
108
|
+
+ `${ref.taskId}.`);
|
|
109
|
+
}
|
|
110
|
+
if (options.store === undefined) {
|
|
111
|
+
throw usageError("job acknowledge requires a Task store to resolve the Leader assertion.");
|
|
112
|
+
}
|
|
113
|
+
// Carry the full managed task caller, including its ephemeral launch key.
|
|
114
|
+
// A durable leaderAssertion by itself is intentionally not sufficient.
|
|
115
|
+
const caller = resolveJobCaller(options.environment, ref.taskId, options.store);
|
|
116
|
+
await ensureFileTaskController(options.home, { environment: options.environment });
|
|
117
|
+
const result = await acknowledgeDurableJob(options.home, ref.taskId, ref.jobId, caller);
|
|
118
|
+
if (options.json === true)
|
|
119
|
+
return `${JSON.stringify(result, null, 2)}\n`;
|
|
120
|
+
return result.acknowledged
|
|
121
|
+
? `Job ${result.job.id} acknowledged (status: ${result.job.status})\n`
|
|
122
|
+
: `Job ${result.job.id} is not in unknown-needs-attention state (${result.job.status})\n`;
|
|
123
|
+
}
|
|
124
|
+
function parseStartArgs(args) {
|
|
125
|
+
let taskId;
|
|
126
|
+
let projectId;
|
|
127
|
+
let head;
|
|
128
|
+
let workspace;
|
|
129
|
+
let owner = { kind: "task" };
|
|
130
|
+
let retryOf;
|
|
131
|
+
const env = {};
|
|
132
|
+
const steps = [];
|
|
133
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
134
|
+
const flag = args[index];
|
|
135
|
+
const value = args[index + 1];
|
|
136
|
+
if (value === undefined || value.startsWith("--")) {
|
|
137
|
+
throw usageError(`Missing value for ${flag}.`, "yui job start ...");
|
|
138
|
+
}
|
|
139
|
+
index += 1;
|
|
140
|
+
switch (flag) {
|
|
141
|
+
case "--task":
|
|
142
|
+
taskId = value;
|
|
143
|
+
break;
|
|
144
|
+
case "--project":
|
|
145
|
+
projectId = value;
|
|
146
|
+
break;
|
|
147
|
+
case "--head":
|
|
148
|
+
head = value;
|
|
149
|
+
break;
|
|
150
|
+
case "--workspace":
|
|
151
|
+
workspace = value;
|
|
152
|
+
break;
|
|
153
|
+
case "--owner":
|
|
154
|
+
owner = parseJobOwner(value);
|
|
155
|
+
break;
|
|
156
|
+
case "--retry-of":
|
|
157
|
+
retryOf = value;
|
|
158
|
+
break;
|
|
159
|
+
case "--env": {
|
|
160
|
+
const separator = value.indexOf("=");
|
|
161
|
+
if (separator <= 0) {
|
|
162
|
+
throw usageError(`Invalid --env value: ${value}.`, "yui job start ...");
|
|
163
|
+
}
|
|
164
|
+
env[value.slice(0, separator)] = value.slice(separator + 1);
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
case "--step": {
|
|
168
|
+
const separator = value.indexOf("=");
|
|
169
|
+
if (separator <= 0) {
|
|
170
|
+
throw usageError(`Invalid --step value: ${value}.`, "yui job start ...");
|
|
171
|
+
}
|
|
172
|
+
steps.push({ name: value.slice(0, separator), command: value.slice(separator + 1) });
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
default:
|
|
176
|
+
throw usageError(`Unknown flag: ${flag}.`, "yui job start ...");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (taskId === undefined || projectId === undefined || head === undefined
|
|
180
|
+
|| workspace === undefined || steps.length === 0) {
|
|
181
|
+
throw usageError("job start requires --task, --project, --head, --workspace, and at least one --step.", "yui job start ...");
|
|
182
|
+
}
|
|
183
|
+
return { taskId, owner, projectId, head, workspace, env, steps, retryOf };
|
|
184
|
+
}
|
|
185
|
+
export function parseJobOwner(value) {
|
|
186
|
+
if (value === "task")
|
|
187
|
+
return { kind: "task" };
|
|
188
|
+
if (value.startsWith("work-item:")) {
|
|
189
|
+
const id = value.slice("work-item:".length);
|
|
190
|
+
if (id.length === 0)
|
|
191
|
+
throw usageError("Invalid --owner work-item id.", "yui job start ...");
|
|
192
|
+
return { kind: "work-item", workItemId: id };
|
|
193
|
+
}
|
|
194
|
+
if (value.startsWith("integration-attempt:")) {
|
|
195
|
+
const id = value.slice("integration-attempt:".length);
|
|
196
|
+
if (id.length === 0) {
|
|
197
|
+
throw usageError("Invalid --owner integration-attempt id.", "yui job start ...");
|
|
198
|
+
}
|
|
199
|
+
return { kind: "integration-attempt", integrationAttemptId: id };
|
|
200
|
+
}
|
|
201
|
+
throw usageError(`Invalid --owner: ${value}. Use ${JOB_OWNER_USAGE}.`, "yui job start ...");
|
|
202
|
+
}
|
|
203
|
+
function parseRefArgs(args, command) {
|
|
204
|
+
let taskId;
|
|
205
|
+
let jobId;
|
|
206
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
207
|
+
const flag = args[index];
|
|
208
|
+
const value = args[index + 1];
|
|
209
|
+
if (value === undefined || value.startsWith("--")) {
|
|
210
|
+
throw usageError(`Missing value for ${flag}.`, `yui job ${command} ...`);
|
|
211
|
+
}
|
|
212
|
+
index += 1;
|
|
213
|
+
if (flag === "--task")
|
|
214
|
+
taskId = value;
|
|
215
|
+
else if (flag === "--job")
|
|
216
|
+
jobId = value;
|
|
217
|
+
else
|
|
218
|
+
throw usageError(`Unknown flag: ${flag}.`, `yui job ${command} ...`);
|
|
219
|
+
}
|
|
220
|
+
if (taskId === undefined || jobId === undefined) {
|
|
221
|
+
throw usageError(`job ${command} requires --task and --job.`, `yui job ${command} ...`);
|
|
222
|
+
}
|
|
223
|
+
return { taskId, jobId };
|
|
224
|
+
}
|
|
225
|
+
function formatOwner(owner) {
|
|
226
|
+
if (owner.kind === "task")
|
|
227
|
+
return "task";
|
|
228
|
+
if (owner.kind === "work-item")
|
|
229
|
+
return `work-item:${owner.workItemId}`;
|
|
230
|
+
return `integration-attempt:${owner.integrationAttemptId}`;
|
|
231
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `yui execution audit` — read-only execution history report (Issue 11 §3).
|
|
3
|
+
*
|
|
4
|
+
* The command opens the Home read-only, aggregates durable records, and never
|
|
5
|
+
* writes state or wakes a Leader. Filtering is by Task and optional ISO time
|
|
6
|
+
* window. Output is a compact text report; `--json` emits the full report.
|
|
7
|
+
*/
|
|
8
|
+
import { usageError } from "../errors/cliError.js";
|
|
9
|
+
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
10
|
+
import { runExecutionAudit } from "../observability/executionAudit.js";
|
|
11
|
+
export function parseExecutionAuditOptions(args) {
|
|
12
|
+
const options = {};
|
|
13
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
14
|
+
const argument = args[index];
|
|
15
|
+
if (argument === "--task") {
|
|
16
|
+
const value = args[++index];
|
|
17
|
+
if (value === undefined || value.startsWith("--")) {
|
|
18
|
+
throw usageError("execution audit --task requires a Task id.");
|
|
19
|
+
}
|
|
20
|
+
options.taskId = value;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (argument === "--since" || argument === "--until") {
|
|
24
|
+
const value = args[++index];
|
|
25
|
+
if (value === undefined || value.startsWith("--")) {
|
|
26
|
+
throw usageError(`execution audit ${argument} requires an ISO timestamp.`);
|
|
27
|
+
}
|
|
28
|
+
const parsed = new Date(value);
|
|
29
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
30
|
+
throw usageError(`execution audit ${argument} timestamp is invalid: ${value}.`);
|
|
31
|
+
}
|
|
32
|
+
if (argument === "--since")
|
|
33
|
+
options.since = parsed;
|
|
34
|
+
else
|
|
35
|
+
options.until = parsed;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
throw usageError("execution audit usage: yui execution audit [--task <id>] [--since <iso>] [--until <iso>].");
|
|
39
|
+
}
|
|
40
|
+
if (options.since !== undefined
|
|
41
|
+
&& options.until !== undefined
|
|
42
|
+
&& options.since.getTime() > options.until.getTime()) {
|
|
43
|
+
throw usageError("execution audit --since must not be after --until.");
|
|
44
|
+
}
|
|
45
|
+
return options;
|
|
46
|
+
}
|
|
47
|
+
export function runExecutionAuditCommand(home, options) {
|
|
48
|
+
const report = runExecutionAudit(home, options);
|
|
49
|
+
return { output: renderExecutionAudit(report), report };
|
|
50
|
+
}
|
|
51
|
+
function formatDuration(milliseconds) {
|
|
52
|
+
if (!Number.isFinite(milliseconds) || milliseconds <= 0)
|
|
53
|
+
return "—";
|
|
54
|
+
const hours = milliseconds / 3_600_000;
|
|
55
|
+
if (hours >= 1)
|
|
56
|
+
return `${hours.toFixed(1)}h`;
|
|
57
|
+
const minutes = milliseconds / 60_000;
|
|
58
|
+
if (minutes >= 1)
|
|
59
|
+
return `${minutes.toFixed(1)}m`;
|
|
60
|
+
return `${(milliseconds / 1_000).toFixed(1)}s`;
|
|
61
|
+
}
|
|
62
|
+
function formatBytes(bytes) {
|
|
63
|
+
if (typeof bytes === "string")
|
|
64
|
+
return bytes;
|
|
65
|
+
if (!Number.isFinite(bytes) || bytes <= 0)
|
|
66
|
+
return "—";
|
|
67
|
+
const units = ["B", "KiB", "MiB", "GiB", "TiB"];
|
|
68
|
+
let value = bytes;
|
|
69
|
+
let unit = 0;
|
|
70
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
71
|
+
value /= 1024;
|
|
72
|
+
unit += 1;
|
|
73
|
+
}
|
|
74
|
+
return `${value.toFixed(value >= 10 || unit === 0 ? 0 : 1)} ${units[unit]}`;
|
|
75
|
+
}
|
|
76
|
+
function sectionError(name, report) {
|
|
77
|
+
const section = report[name];
|
|
78
|
+
if (section?.status === "error") {
|
|
79
|
+
return [`${name}: read error — ${section.error ?? "unknown error"}`];
|
|
80
|
+
}
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
export function renderExecutionAudit(report, width = defaultTableWidth()) {
|
|
84
|
+
const lines = [
|
|
85
|
+
`Execution audit for ${report.home}`,
|
|
86
|
+
`Generated ${report.generatedAt} · Home identity ${report.homeIdentity}`,
|
|
87
|
+
`Scope: ${report.scope.taskId ?? "all Tasks"}${report.scope.since === undefined ? "" : ` since ${report.scope.since}`}${report.scope.until === undefined ? "" : ` until ${report.scope.until}`}`
|
|
88
|
+
];
|
|
89
|
+
if (report.tasks.status === "ok" && report.tasks.data !== undefined) {
|
|
90
|
+
lines.push("", `Tasks: ${report.tasks.data.total} total · ${report.tasks.data.archived} archived · ${report.tasks.data.active} active`);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
lines.push("", ...sectionError("tasks", report));
|
|
94
|
+
}
|
|
95
|
+
if (report.runs.status === "ok" && report.runs.data !== undefined) {
|
|
96
|
+
const runs = report.runs.data;
|
|
97
|
+
lines.push("", `Agent runs: ${runs.total} total · ${runs.failed} failed (${(runs.failureRate * 100).toFixed(1)}%) · ${runs.yielded} yielded · ${runs.active} active`, `Duration: ${formatDuration(runs.cumulativeDurationMs)} total · ${formatDuration(runs.failedDurationMs)} in failed runs`, `By role: ${runs.byRole.leader} leader · ${runs.byRole.reviewer} reviewer · ${runs.byRole.implementer} implementer/worker · ${runs.byRole.other} other`, `By purpose: ${runs.byPurpose.execution} execution · ${runs.byPurpose.review} review`);
|
|
98
|
+
const faultRows = Object.entries(runs.faultClasses)
|
|
99
|
+
.filter(([, count]) => count > 0)
|
|
100
|
+
.sort((left, right) => right[1] - left[1]);
|
|
101
|
+
if (faultRows.length > 0) {
|
|
102
|
+
lines.push(renderTable("Run failure classes", [
|
|
103
|
+
{ header: "Class", minWidth: 24, maxWidth: 40 },
|
|
104
|
+
{ header: "Count", minWidth: 5, maxWidth: 8 }
|
|
105
|
+
], faultRows.map(([name, count]) => [name, String(count)]), width));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
lines.push("", ...sectionError("runs", report));
|
|
110
|
+
}
|
|
111
|
+
if (report.wakes.status === "ok" && report.wakes.data !== undefined) {
|
|
112
|
+
const wakes = report.wakes.data;
|
|
113
|
+
lines.push("", `Leader wakes: ${wakes.leaderRuns} runs · ${wakes.withWakeReasons} with reasons · ${wakes.orphanWakes} orphan wakes (${wakes.orphanYieldOnly} yield-only)`);
|
|
114
|
+
if (wakes.suppressedWakes.status === "unsupported") {
|
|
115
|
+
lines.push("Suppressed wakes: unsupported (no quiescence producer in this build)");
|
|
116
|
+
}
|
|
117
|
+
const reasonRows = Object.entries(wakes.byReason).slice(0, 8);
|
|
118
|
+
if (reasonRows.length > 0) {
|
|
119
|
+
lines.push(renderTable("Wake reasons", [
|
|
120
|
+
{ header: "Reason", minWidth: 20, maxWidth: 40 },
|
|
121
|
+
{ header: "Count", minWidth: 5, maxWidth: 8 }
|
|
122
|
+
], reasonRows.map(([name, count]) => [name, String(count)]), width));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
lines.push("", ...sectionError("wakes", report));
|
|
127
|
+
}
|
|
128
|
+
if (report.sessions.status === "ok" && report.sessions.data !== undefined) {
|
|
129
|
+
const sessions = report.sessions.data;
|
|
130
|
+
lines.push("", `Sessions: ${sessions.generations} generations · ${sessions.broken} broken · ${sessions.stopped} stopped · ${sessions.other} other`, `Resets: ${sessions.resets} · lifecycle events ${sessions.lifecycleEvents} · stop failures ${sessions.stopFailures}`);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
lines.push("", ...sectionError("sessions", report));
|
|
134
|
+
}
|
|
135
|
+
if (report.reviews.status === "ok" && report.reviews.data !== undefined) {
|
|
136
|
+
const reviews = report.reviews.data;
|
|
137
|
+
lines.push("", `Review rounds: ${reviews.total} total · ${reviews.completed} completed · ${reviews.failed} failed`, `Execution failures: ${reviews.infraFailed} · semantic negatives: ${reviews.semanticNegative}`);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
lines.push("", ...sectionError("reviews", report));
|
|
141
|
+
}
|
|
142
|
+
if (report.integrations.status === "ok" && report.integrations.data !== undefined) {
|
|
143
|
+
const integrations = report.integrations.data;
|
|
144
|
+
lines.push("", `Integration attempts: ${integrations.total} total · ${integrations.committed} committed · ${integrations.failed} failed · ${integrations.superseded} superseded`, `Failure classes: ${integrations.environmentFailures} environment · ${integrations.staleCasFailures} stale-base/CAS · ${integrations.candidateFailures} candidate`, `Gate reuse: ${integrations.gateReuse} attempt(s) reused an exact (commit, checks) signature`);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
lines.push("", ...sectionError("integrations", report));
|
|
148
|
+
}
|
|
149
|
+
if (report.events.status === "ok" && report.events.data !== undefined) {
|
|
150
|
+
const events = report.events.data;
|
|
151
|
+
lines.push("", `Events: ${events.total} total · ${events.progressEvents} provider-progress (${(events.progressShare * 100).toFixed(1)}%) · ${events.semanticEvents} semantic · ${events.obsoleteEvents} obsolete`, `Messages: ${events.messages}`);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
lines.push("", ...sectionError("events", report));
|
|
155
|
+
}
|
|
156
|
+
if (report.workItems.status === "ok" && report.workItems.data !== undefined) {
|
|
157
|
+
const items = report.workItems.data;
|
|
158
|
+
lines.push("", `Work items: ${items.total} total · ${items.completed} completed · ${items.retired} retired`);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
lines.push("", ...sectionError("workItems", report));
|
|
162
|
+
}
|
|
163
|
+
if (report.storage.status === "ok" && report.storage.data !== undefined) {
|
|
164
|
+
const storage = report.storage.data;
|
|
165
|
+
lines.push("", `Storage: backend ${storage.backend} · state.json ${formatBytes(storage.stateJsonBytes)} · yui.db ${formatBytes(storage.databaseBytes)}`
|
|
166
|
+
+ ` · runtime/ ${formatBytes(storage.runtimeDirBytes)} · deployments ${formatBytes(storage.deploymentsBytes)}`);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
lines.push("", ...sectionError("storage", report));
|
|
170
|
+
}
|
|
171
|
+
if (report.topLongRunning.status === "ok" && report.topLongRunning.data !== undefined) {
|
|
172
|
+
const entries = report.topLongRunning.data;
|
|
173
|
+
if (entries.length > 0) {
|
|
174
|
+
lines.push("", renderTable("Top long-running runs", [
|
|
175
|
+
{ header: "Task", minWidth: 8, maxWidth: 14 },
|
|
176
|
+
{ header: "Run", minWidth: 14, maxWidth: 24 },
|
|
177
|
+
{ header: "Role", minWidth: 10, maxWidth: 20 },
|
|
178
|
+
{ header: "Status", minWidth: 8, maxWidth: 10 },
|
|
179
|
+
{ header: "Duration", minWidth: 8, maxWidth: 10 }
|
|
180
|
+
], entries.map((entry) => [
|
|
181
|
+
entry.taskId,
|
|
182
|
+
entry.runId,
|
|
183
|
+
entry.roleName,
|
|
184
|
+
entry.status,
|
|
185
|
+
formatDuration(entry.durationMs)
|
|
186
|
+
]), width));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
lines.push("", ...sectionError("topLongRunning", report));
|
|
191
|
+
}
|
|
192
|
+
return lines.join("\n");
|
|
193
|
+
}
|