@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
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `yui resources gc` — Resource GC command (Issue 10).
|
|
3
|
+
*
|
|
4
|
+
* Dry-run by default. `--apply` quarantines releasable resources only when
|
|
5
|
+
* `resources.gcMode=quarantine` is set; `--purge` permanently deletes
|
|
6
|
+
* quarantined resources after the observation window; `--restore` rolls back
|
|
7
|
+
* every quarantined resource to its original path.
|
|
8
|
+
*/
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
import { usageError } from "../errors/cliError.js";
|
|
11
|
+
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
12
|
+
import { applyResourceGc, planResourceGc, purgeResourceQuarantine, restoreAllResourceGc } from "../resources/resourceGc.js";
|
|
13
|
+
import { createResourceRegistryStore } from "../resources/resourceRegistryStore.js";
|
|
14
|
+
import { resourceKindLabel, resourceOwnerLabel } from "../resources/resourceDiscovery.js";
|
|
15
|
+
import { resolveResourcesGcMode } from "../config/yuiConfig.js";
|
|
16
|
+
export async function runResourcesCommand(args, store, options = {}) {
|
|
17
|
+
const [command, ...rest] = args;
|
|
18
|
+
if (command !== "gc") {
|
|
19
|
+
throw usageError("Unknown resources command. Available: yui resources gc "
|
|
20
|
+
+ "[--dry-run|--apply|--purge|--restore] [--quarantine-ttl-hours <hours>].");
|
|
21
|
+
}
|
|
22
|
+
return runGcCommand(rest, store, options);
|
|
23
|
+
}
|
|
24
|
+
async function runGcCommand(args, store, options) {
|
|
25
|
+
const action = parseGcAction(args);
|
|
26
|
+
const ttlHours = parseTtlHours(args);
|
|
27
|
+
const now = options.now?.() ?? new Date();
|
|
28
|
+
const home = resolve(store.rootDirectory());
|
|
29
|
+
const mode = resolveGcMode(store);
|
|
30
|
+
const projects = store.listProjects();
|
|
31
|
+
const managedWorkspaces = collectManagedWorkspaces(store);
|
|
32
|
+
const taskStatusById = collectTaskStatuses(store);
|
|
33
|
+
const activeWorkspaceOwnerPaths = collectActiveWorkspaceOwnerPaths(store);
|
|
34
|
+
if (action === "restore") {
|
|
35
|
+
const result = await restoreAllResourceGc(home, { now });
|
|
36
|
+
return {
|
|
37
|
+
output: renderRestoreResult(result),
|
|
38
|
+
data: result
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (action === "purge") {
|
|
42
|
+
if (mode !== "quarantine") {
|
|
43
|
+
return {
|
|
44
|
+
output: "resources.gcMode=report; purge skipped. "
|
|
45
|
+
+ "Set resources.gcMode=quarantine to enable purge.",
|
|
46
|
+
data: { mode, action: "purge", skipped: true }
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const result = await purgeResourceQuarantine(home, { now, ttlHours, managedWorkspaces });
|
|
50
|
+
return {
|
|
51
|
+
output: renderPurgeResult(result),
|
|
52
|
+
data: result
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const plan = await planResourceGc({
|
|
56
|
+
home,
|
|
57
|
+
registryStore: createResourceRegistryStore(home),
|
|
58
|
+
projects,
|
|
59
|
+
managedWorkspaces,
|
|
60
|
+
taskStatusById,
|
|
61
|
+
mode,
|
|
62
|
+
now,
|
|
63
|
+
quarantineTtlHours: ttlHours,
|
|
64
|
+
activeWorkspaceOwnerPaths
|
|
65
|
+
});
|
|
66
|
+
if (action === "apply" && mode === "quarantine") {
|
|
67
|
+
const result = await applyResourceGc({
|
|
68
|
+
home,
|
|
69
|
+
registryStore: createResourceRegistryStore(home),
|
|
70
|
+
projects,
|
|
71
|
+
managedWorkspaces,
|
|
72
|
+
taskStatusById,
|
|
73
|
+
mode,
|
|
74
|
+
now,
|
|
75
|
+
quarantineTtlHours: ttlHours,
|
|
76
|
+
activeWorkspaceOwnerPaths
|
|
77
|
+
}, plan);
|
|
78
|
+
return {
|
|
79
|
+
output: renderApplyResult(result),
|
|
80
|
+
data: result
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
output: renderPlan(plan, action),
|
|
85
|
+
data: plan
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function parseGcAction(args) {
|
|
89
|
+
const flags = new Set(args);
|
|
90
|
+
if (flags.has("--apply"))
|
|
91
|
+
return "apply";
|
|
92
|
+
if (flags.has("--purge"))
|
|
93
|
+
return "purge";
|
|
94
|
+
if (flags.has("--restore"))
|
|
95
|
+
return "restore";
|
|
96
|
+
return "dry-run";
|
|
97
|
+
}
|
|
98
|
+
function parseTtlHours(args) {
|
|
99
|
+
const index = args.indexOf("--quarantine-ttl-hours");
|
|
100
|
+
if (index === -1)
|
|
101
|
+
return 24;
|
|
102
|
+
const value = Number(args[index + 1]);
|
|
103
|
+
if (!Number.isFinite(value) || value < 1 || value > 24 * 30) {
|
|
104
|
+
throw usageError("Quarantine TTL hours must be between 1 and 720.");
|
|
105
|
+
}
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
function resolveGcMode(store) {
|
|
109
|
+
return resolveResourcesGcMode(store.getConfig().resourcesGcMode);
|
|
110
|
+
}
|
|
111
|
+
function collectManagedWorkspaces(store) {
|
|
112
|
+
const workspaces = [];
|
|
113
|
+
for (const task of store.listTasks()) {
|
|
114
|
+
workspaces.push(...store.listManagedWorkspaces(task.id));
|
|
115
|
+
}
|
|
116
|
+
return workspaces;
|
|
117
|
+
}
|
|
118
|
+
function collectTaskStatuses(store) {
|
|
119
|
+
const statuses = new Map();
|
|
120
|
+
for (const task of store.listTasks()) {
|
|
121
|
+
statuses.set(task.id, task.status);
|
|
122
|
+
}
|
|
123
|
+
return statuses;
|
|
124
|
+
}
|
|
125
|
+
/** Workspace paths claimed by active durable Jobs (AgentRuns). */
|
|
126
|
+
function collectActiveWorkspaceOwnerPaths(store) {
|
|
127
|
+
const paths = [];
|
|
128
|
+
for (const task of store.listTasks()) {
|
|
129
|
+
for (const run of store.listAgentRuns(task.id)) {
|
|
130
|
+
if (run.status !== "active")
|
|
131
|
+
continue;
|
|
132
|
+
const workspace = run.workspace;
|
|
133
|
+
if (workspace === undefined)
|
|
134
|
+
continue;
|
|
135
|
+
paths.push(workspace.root, ...workspace.entries.map((entry) => entry.path));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return paths;
|
|
139
|
+
}
|
|
140
|
+
function renderPlan(plan, action) {
|
|
141
|
+
const lines = [];
|
|
142
|
+
const modeNotice = plan.mode === "report" && action === "apply"
|
|
143
|
+
? "resources.gcMode=report; apply shadowed (no changes). "
|
|
144
|
+
+ "Set resources.gcMode=quarantine to enable.\n"
|
|
145
|
+
: "";
|
|
146
|
+
lines.push(`${modeNotice}Resource GC plan (${plan.mode}, ${action}) at ${plan.generatedAt}`);
|
|
147
|
+
lines.push(`Home: ${plan.home}`);
|
|
148
|
+
lines.push("");
|
|
149
|
+
if (plan.scan.diagnostics.length > 0) {
|
|
150
|
+
lines.push("Scan diagnostics:");
|
|
151
|
+
for (const diag of plan.scan.diagnostics) {
|
|
152
|
+
lines.push(` [${diag.severity}] ${diag.source}: ${diag.message}`);
|
|
153
|
+
}
|
|
154
|
+
lines.push("");
|
|
155
|
+
}
|
|
156
|
+
if (plan.records.length === 0) {
|
|
157
|
+
lines.push("No resources discovered.");
|
|
158
|
+
return lines.join("\n");
|
|
159
|
+
}
|
|
160
|
+
const rows = plan.records.map((record) => [
|
|
161
|
+
record.id,
|
|
162
|
+
resourceKindLabel(record.kind),
|
|
163
|
+
truncate(record.path, 60),
|
|
164
|
+
resourceOwnerLabel(record.owner),
|
|
165
|
+
formatSize(record.sizeBytes),
|
|
166
|
+
record.cleanliness,
|
|
167
|
+
record.activeRefs.length === 0 ? "-" : `${record.activeRefs.length} ref(s)`,
|
|
168
|
+
record.disposition,
|
|
169
|
+
record.blocker ?? ""
|
|
170
|
+
]);
|
|
171
|
+
lines.push(renderTable("Resources", [
|
|
172
|
+
{ header: "ID", minWidth: 16, maxWidth: 16 },
|
|
173
|
+
{ header: "Kind", minWidth: 14, maxWidth: 14 },
|
|
174
|
+
{ header: "Path", minWidth: 20, maxWidth: 60 },
|
|
175
|
+
{ header: "Owner", minWidth: 16, maxWidth: 32 },
|
|
176
|
+
{ header: "Size", minWidth: 10, maxWidth: 12 },
|
|
177
|
+
{ header: "Clean", minWidth: 8, maxWidth: 8 },
|
|
178
|
+
{ header: "Refs", minWidth: 8, maxWidth: 10 },
|
|
179
|
+
{ header: "Disposition", minWidth: 14, maxWidth: 16 },
|
|
180
|
+
{ header: "Blocker", minWidth: 10, maxWidth: 40 }
|
|
181
|
+
], rows, defaultTableWidth()));
|
|
182
|
+
lines.push("");
|
|
183
|
+
lines.push(`Summary: ${plan.releasable.length} releasable, `
|
|
184
|
+
+ `${plan.retained.length} retained, `
|
|
185
|
+
+ `${plan.quarantined.length} quarantined, `
|
|
186
|
+
+ `${plan.deleted.length} deleted.`);
|
|
187
|
+
return lines.join("\n");
|
|
188
|
+
}
|
|
189
|
+
function renderApplyResult(result) {
|
|
190
|
+
const lines = [];
|
|
191
|
+
lines.push(`Resource GC apply at ${result.planned.generatedAt}`);
|
|
192
|
+
lines.push(`Home: ${result.planned.home}`);
|
|
193
|
+
lines.push("");
|
|
194
|
+
if (result.applied.length > 0) {
|
|
195
|
+
lines.push(`Quarantined ${result.applied.length} resource(s):`);
|
|
196
|
+
for (const record of result.applied) {
|
|
197
|
+
lines.push(` ${record.id} ${resourceKindLabel(record.kind)} ${record.path}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (result.failed.length > 0) {
|
|
201
|
+
lines.push(`Failed ${result.failed.length} resource(s):`);
|
|
202
|
+
for (const record of result.failed) {
|
|
203
|
+
lines.push(` ${record.id} ${record.path}: ${record.blocker ?? "unknown"}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (result.restored.length > 0) {
|
|
207
|
+
lines.push(`Restored ${result.restored.length} resource(s) from quarantine.`);
|
|
208
|
+
}
|
|
209
|
+
if (result.applied.length === 0 && result.failed.length === 0 && result.restored.length === 0) {
|
|
210
|
+
lines.push("No releasable resources.");
|
|
211
|
+
}
|
|
212
|
+
return lines.join("\n");
|
|
213
|
+
}
|
|
214
|
+
function renderRestoreResult(result) {
|
|
215
|
+
const lines = [];
|
|
216
|
+
lines.push(`Resource GC restore at ${result.planned.generatedAt}`);
|
|
217
|
+
lines.push(`Home: ${result.planned.home}`);
|
|
218
|
+
lines.push("");
|
|
219
|
+
if (result.restored.length > 0) {
|
|
220
|
+
lines.push(`Restored ${result.restored.length} resource(s):`);
|
|
221
|
+
for (const record of result.restored) {
|
|
222
|
+
lines.push(` ${record.id} ${resourceKindLabel(record.kind)} ${record.path}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (result.failed.length > 0) {
|
|
226
|
+
lines.push(`Failed ${result.failed.length} resource(s):`);
|
|
227
|
+
for (const record of result.failed) {
|
|
228
|
+
lines.push(` ${record.id}: ${record.blocker ?? "unknown"}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (result.restored.length === 0 && result.failed.length === 0) {
|
|
232
|
+
lines.push("No quarantined resources to restore.");
|
|
233
|
+
}
|
|
234
|
+
return lines.join("\n");
|
|
235
|
+
}
|
|
236
|
+
function renderPurgeResult(result) {
|
|
237
|
+
const lines = [];
|
|
238
|
+
lines.push(`Resource GC purge at ${result.planned.generatedAt}`);
|
|
239
|
+
lines.push(`Home: ${result.planned.home}`);
|
|
240
|
+
lines.push("");
|
|
241
|
+
if (result.purged.length > 0) {
|
|
242
|
+
lines.push(`Purged ${result.purged.length} resource(s):`);
|
|
243
|
+
for (const record of result.purged) {
|
|
244
|
+
lines.push(` ${record.id} ${resourceKindLabel(record.kind)}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (result.restored.length > 0) {
|
|
248
|
+
lines.push(`Restored ${result.restored.length} resource(s) with new live references.`);
|
|
249
|
+
}
|
|
250
|
+
if (result.failed.length > 0) {
|
|
251
|
+
lines.push(`Failed ${result.failed.length} resource(s):`);
|
|
252
|
+
for (const record of result.failed) {
|
|
253
|
+
lines.push(` ${record.id}: ${record.blocker ?? "unknown"}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (result.purged.length === 0 && result.restored.length === 0 && result.failed.length === 0) {
|
|
257
|
+
lines.push("No quarantined resources past the observation window.");
|
|
258
|
+
}
|
|
259
|
+
return lines.join("\n");
|
|
260
|
+
}
|
|
261
|
+
function formatSize(bytes) {
|
|
262
|
+
if (bytes === undefined)
|
|
263
|
+
return "?";
|
|
264
|
+
if (bytes < 1024)
|
|
265
|
+
return `${bytes} B`;
|
|
266
|
+
if (bytes < 1024 * 1024)
|
|
267
|
+
return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
268
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
269
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
|
|
270
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
|
|
271
|
+
}
|
|
272
|
+
function truncate(value, max) {
|
|
273
|
+
return value.length <= max ? value : `…${value.slice(-(max - 1))}`;
|
|
274
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { usageError } from "../errors/cliError.js";
|
|
2
|
+
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
3
|
+
export function parseSessionReconcileOptions(args) {
|
|
4
|
+
const allowed = new Set(["--report", "--cleanup"]);
|
|
5
|
+
if (args.some((argument) => !allowed.has(argument))) {
|
|
6
|
+
throw usageError("Session reconcile usage: yui session reconcile [--report] [--cleanup].");
|
|
7
|
+
}
|
|
8
|
+
return {
|
|
9
|
+
report: args.includes("--report") || !args.includes("--cleanup"),
|
|
10
|
+
cleanup: args.includes("--cleanup")
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* `yui session reconcile` — read-only durable/physical reconciliation report
|
|
15
|
+
* by default. `--cleanup` performs exact-owner termination of live roots
|
|
16
|
+
* belonging to terminal/archived Tasks.
|
|
17
|
+
*/
|
|
18
|
+
export async function runSessionReconcileCommand(input) {
|
|
19
|
+
if (input.options.cleanup) {
|
|
20
|
+
const before = input.reconciliation.report();
|
|
21
|
+
const targets = before.entries.filter((entry) => entry.archiveBlocked);
|
|
22
|
+
for (const entry of targets) {
|
|
23
|
+
const owner = entry.owner.scope === "task"
|
|
24
|
+
? {
|
|
25
|
+
scope: "task",
|
|
26
|
+
taskId: entry.owner.taskId,
|
|
27
|
+
roleName: entry.owner.roleName
|
|
28
|
+
}
|
|
29
|
+
: {
|
|
30
|
+
scope: "global",
|
|
31
|
+
roleName: entry.owner.roleName
|
|
32
|
+
};
|
|
33
|
+
await input.reconciliation.terminateOwner(owner);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const report = input.reconciliation.report();
|
|
37
|
+
const exitCode = report.summary.archiveBlockers > 0 ? 5 : 0;
|
|
38
|
+
return {
|
|
39
|
+
output: renderSessionReconciliationReport(report),
|
|
40
|
+
data: report,
|
|
41
|
+
exitCode
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function renderSessionReconciliationReport(report, width = defaultTableWidth()) {
|
|
45
|
+
const lines = [
|
|
46
|
+
"Session reconciliation",
|
|
47
|
+
"",
|
|
48
|
+
`Owners: ${report.summary.owners}; live physical roots: `
|
|
49
|
+
+ `${report.summary.livePhysicalRoots}; archive blockers: `
|
|
50
|
+
+ `${report.summary.archiveBlockers}; verification gaps: `
|
|
51
|
+
+ `${report.summary.verificationGaps}.`
|
|
52
|
+
];
|
|
53
|
+
if (report.entries.length === 0) {
|
|
54
|
+
lines.push("", "No Session owner records found.");
|
|
55
|
+
return lines.join("\n");
|
|
56
|
+
}
|
|
57
|
+
lines.push("", renderTable("Session owners", [
|
|
58
|
+
{ header: "Task", minWidth: 8, maxWidth: 18 },
|
|
59
|
+
{ header: "Role", minWidth: 12, maxWidth: 22 },
|
|
60
|
+
{ header: "Agent", minWidth: 7, maxWidth: 14 },
|
|
61
|
+
{ header: "Durable", minWidth: 8, maxWidth: 10 },
|
|
62
|
+
{ header: "Physical", minWidth: 8, maxWidth: 10 },
|
|
63
|
+
{ header: "PID", minWidth: 5, maxWidth: 8 },
|
|
64
|
+
{ header: "RSS", minWidth: 8, maxWidth: 11 },
|
|
65
|
+
{ header: "Children", minWidth: 8, maxWidth: 8 },
|
|
66
|
+
{ header: "Last stop", minWidth: 12, maxWidth: 16 },
|
|
67
|
+
{ header: "Mismatch", minWidth: 12, maxWidth: 30 },
|
|
68
|
+
{ header: "Archive", minWidth: 7, maxWidth: 9 }
|
|
69
|
+
], report.entries.map((entry) => [
|
|
70
|
+
entry.owner.scope === "global"
|
|
71
|
+
? "global"
|
|
72
|
+
: (entry.owner.taskId ?? "—"),
|
|
73
|
+
entry.owner.roleName,
|
|
74
|
+
entry.agentId,
|
|
75
|
+
entry.durableStatus,
|
|
76
|
+
entry.physical === undefined
|
|
77
|
+
? "gap"
|
|
78
|
+
: entry.physical.alive
|
|
79
|
+
? "live"
|
|
80
|
+
: "absent",
|
|
81
|
+
entry.physical === undefined ? "—" : String(entry.physical.pid),
|
|
82
|
+
entry.physical === undefined || entry.physical.rssBytes <= 0
|
|
83
|
+
? "—"
|
|
84
|
+
: formatBytes(entry.physical.rssBytes),
|
|
85
|
+
entry.physical === undefined ? "—" : String(entry.physical.childCount),
|
|
86
|
+
entry.lastStopOutcome ?? "—",
|
|
87
|
+
entry.mismatch ?? "—",
|
|
88
|
+
entry.archiveBlocked ? "blocked" : "ok"
|
|
89
|
+
]), width));
|
|
90
|
+
return lines.join("\n");
|
|
91
|
+
}
|
|
92
|
+
function formatBytes(bytes) {
|
|
93
|
+
if (bytes <= 0)
|
|
94
|
+
return "—";
|
|
95
|
+
const units = ["B", "KiB", "MiB", "GiB", "TiB"];
|
|
96
|
+
let value = bytes;
|
|
97
|
+
let unit = 0;
|
|
98
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
99
|
+
value /= 1024;
|
|
100
|
+
unit += 1;
|
|
101
|
+
}
|
|
102
|
+
const digits = value >= 10 || unit === 0 ? 0 : 1;
|
|
103
|
+
return `${value.toFixed(digits)} ${units[unit]}`;
|
|
104
|
+
}
|
|
@@ -4,6 +4,8 @@ import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
|
|
|
4
4
|
const LEADER_ROLE = "leader";
|
|
5
5
|
const LEADER_ACTION_RUN_ENV = "YUI_LEADER_ACTION_RUN_ID";
|
|
6
6
|
const LEADER_ACTION_RECEIPT_ENV = "YUI_LEADER_ACTION_RECEIPT_ID";
|
|
7
|
+
/** rr13: Per-Session DurableJob caller key injected at native Session launch. */
|
|
8
|
+
const JOB_CALLER_KEY_ENV = "YUI_JOB_CALLER_KEY";
|
|
7
9
|
export function taskActor(environment, taskId) {
|
|
8
10
|
const env = environment ?? {};
|
|
9
11
|
if (env.YUI_SESSION_SCOPE === "task"
|
|
@@ -27,6 +29,121 @@ export function taskActor(environment, taskId) {
|
|
|
27
29
|
}
|
|
28
30
|
return "user";
|
|
29
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* rr8: Resolve the caller identity for a `job.start`/`job.cancel` request from
|
|
34
|
+
* the managed Session environment. The Controller binds the declared job owner
|
|
35
|
+
* to this identity — a Reviewer is rejected outright, a Worker can only touch
|
|
36
|
+
* its own Work Item's jobs, and a Leader or plain user retains full access.
|
|
37
|
+
*
|
|
38
|
+
* rr12: The identity is now Controller-verified rather than self-reported:
|
|
39
|
+
* - A managed Task Session (`YUI_SESSION_SCOPE=task`) returns `scope: "task"`
|
|
40
|
+
* with its Role and Run. A Leader additionally carries the current in-flight
|
|
41
|
+
* Turn receipt (preferring the explicit `YUI_LEADER_ACTION_*` assertion over
|
|
42
|
+
* a possibly-stale `YUI_RUN_ID`), which the Controller verifies through the
|
|
43
|
+
* same active-Run + in-flight + Session check as `job.acknowledge`.
|
|
44
|
+
*
|
|
45
|
+
* rr13: A managed Task Session also carries `callerKey` — the
|
|
46
|
+
* `YUI_JOB_CALLER_KEY` injected at its native Session launch. The Controller
|
|
47
|
+
* hashes it and compares against the durable `jobCallerKeyHashes` map, so a
|
|
48
|
+
* client that reads durable state cannot replay the caller. A `user`-scope
|
|
49
|
+
* caller is rejected outright for job.start/job.cancel (fail-closed); the
|
|
50
|
+
* human operator acts through the Leader Session.
|
|
51
|
+
*
|
|
52
|
+
* A managed Task Session may only start jobs for its own Task. An incomplete
|
|
53
|
+
* managed identity (role/agent/run/session vars without a scope) is rejected
|
|
54
|
+
* rather than silently downgraded to user authority.
|
|
55
|
+
*/
|
|
56
|
+
export function resolveJobCaller(environment, taskId, store) {
|
|
57
|
+
const env = environment ?? {};
|
|
58
|
+
if (env.YUI_SESSION_SCOPE === "task") {
|
|
59
|
+
if (env.YUI_TASK_ID !== taskId) {
|
|
60
|
+
throw usageError("A managed Task Session may not start Jobs for a different Task.");
|
|
61
|
+
}
|
|
62
|
+
const role = env.YUI_ROLE;
|
|
63
|
+
// rr13: Carry the per-Session caller key so the Controller can verify the
|
|
64
|
+
// channel binding. Absent on a managed Session = fail-closed at the
|
|
65
|
+
// Controller boundary.
|
|
66
|
+
const callerKey = env[JOB_CALLER_KEY_ENV];
|
|
67
|
+
if (role === LEADER_ROLE) {
|
|
68
|
+
// Prefer the explicit current-turn Leader assertion over a possibly
|
|
69
|
+
// stale YUI_RUN_ID/launch. The Controller verifies it against the
|
|
70
|
+
// active in-flight Leader Run.
|
|
71
|
+
const assertion = leaderActionAssertion(env);
|
|
72
|
+
if (assertion !== undefined && assertion !== "invalid") {
|
|
73
|
+
return {
|
|
74
|
+
scope: "task",
|
|
75
|
+
taskId,
|
|
76
|
+
role,
|
|
77
|
+
runId: assertion.runId,
|
|
78
|
+
receiptId: assertion.receiptId,
|
|
79
|
+
...(callerKey === undefined ? {} : { callerKey })
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
scope: "task",
|
|
85
|
+
taskId,
|
|
86
|
+
role,
|
|
87
|
+
...(env.YUI_RUN_ID === undefined ? {} : { runId: env.YUI_RUN_ID }),
|
|
88
|
+
...(callerKey === undefined ? {} : { callerKey })
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (env.YUI_SESSION_SCOPE === "global") {
|
|
92
|
+
return userCaller(store, taskId);
|
|
93
|
+
}
|
|
94
|
+
if (env.YUI_ROLE !== undefined
|
|
95
|
+
|| env.YUI_AGENT_ID !== undefined
|
|
96
|
+
|| env.YUI_RUN_ID !== undefined
|
|
97
|
+
|| env.YUI_NATIVE_SESSION_ID !== undefined) {
|
|
98
|
+
throw usageError("Managed Agent identity is incomplete; refusing to infer user authority.");
|
|
99
|
+
}
|
|
100
|
+
return userCaller(store, taskId);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* rr12: Build a `scope: "user"` caller. When a store is available, attach the
|
|
104
|
+
* active in-flight Leader assertion so the Controller can verify the request
|
|
105
|
+
* acts under real Leader authority. Without a store, return the bare
|
|
106
|
+
* `{scope: "user"}` which the Controller rejects (fail-closed).
|
|
107
|
+
*/
|
|
108
|
+
function userCaller(store, taskId) {
|
|
109
|
+
if (store === undefined)
|
|
110
|
+
return { scope: "user" };
|
|
111
|
+
const runId = activeLeaderRunId(store, taskId);
|
|
112
|
+
if (runId === undefined) {
|
|
113
|
+
throw usageError("job.start/job.cancel user scope requires an active in-flight Task Leader: "
|
|
114
|
+
+ `${taskId}.`);
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
scope: "user",
|
|
118
|
+
leaderAssertion: {
|
|
119
|
+
runId,
|
|
120
|
+
receiptId: formatAgentRunReceiptId(taskId, runId)
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* rr12: Resolve the current in-flight Task Leader Run for a non-managed
|
|
126
|
+
* (operator/bare-shell) caller. Unlike `taskLeaderActionRunId`, this does not
|
|
127
|
+
* require managed Leader environment variables — it verifies the durable
|
|
128
|
+
* state directly: an active Leader Run whose Role Session is currently
|
|
129
|
+
* in-flight with the matching receipt. Returns undefined when no Leader is
|
|
130
|
+
* active or in flight.
|
|
131
|
+
*/
|
|
132
|
+
function activeLeaderRunId(store, taskId) {
|
|
133
|
+
const run = store.getActiveAgentRun(taskId, LEADER_ROLE);
|
|
134
|
+
if (run === null || run.status !== "active" || run.roleName !== LEADER_ROLE) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
const sessions = store.getTaskRoleSessionSet(taskId, LEADER_ROLE);
|
|
138
|
+
const expectedReceipt = formatAgentRunReceiptId(taskId, run.id);
|
|
139
|
+
if (sessions === null
|
|
140
|
+
|| sessions.inFlight === null
|
|
141
|
+
|| sessions.inFlight.runId !== run.id
|
|
142
|
+
|| sessions.inFlight.receiptId !== expectedReceipt) {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
return run.id;
|
|
146
|
+
}
|
|
30
147
|
/**
|
|
31
148
|
* Resolve an exact current Task Leader Run for event attribution.
|
|
32
149
|
*
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { usageError } from "../errors/cliError.js";
|
|
2
|
+
import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
|
|
3
|
+
/**
|
|
4
|
+
* Read-only ChangeSet inspection, including its optional integration manifest.
|
|
5
|
+
*/
|
|
6
|
+
export async function runTaskChangeSetCommand(args, store) {
|
|
7
|
+
const [command, ...rest] = args;
|
|
8
|
+
if (command === "show")
|
|
9
|
+
return show(rest, store);
|
|
10
|
+
throw usageError(command === undefined
|
|
11
|
+
? "Task ChangeSet command is required."
|
|
12
|
+
: `Unknown command: task change-set ${command}`);
|
|
13
|
+
}
|
|
14
|
+
function show(args, store) {
|
|
15
|
+
if (args.length !== 1) {
|
|
16
|
+
throw usageError("Task ChangeSet show usage: yui task change-set show <task>/<change-set>.");
|
|
17
|
+
}
|
|
18
|
+
let reference;
|
|
19
|
+
try {
|
|
20
|
+
reference = resolveTaskRecordReference(args[0], {
|
|
21
|
+
kind: "changeSet",
|
|
22
|
+
label: "ChangeSet"
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
throw usageError(error instanceof Error ? error.message : String(error));
|
|
27
|
+
}
|
|
28
|
+
const changeSet = store.getChangeSet(reference.taskId, reference.localId);
|
|
29
|
+
if (changeSet === null) {
|
|
30
|
+
throw usageError(`ChangeSet not found: ${reference.taskId}/${reference.localId}.`);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
output: renderChangeSet(changeSet),
|
|
34
|
+
data: { changeSet }
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function renderChangeSet(changeSet) {
|
|
38
|
+
const lines = [
|
|
39
|
+
`ChangeSet: ${changeSet.id}`,
|
|
40
|
+
`Task: ${changeSet.taskId}`,
|
|
41
|
+
`WorkItem: ${changeSet.workItemId}`,
|
|
42
|
+
`Project: ${changeSet.projectId}`,
|
|
43
|
+
`Base: ${changeSet.baseCommit}`,
|
|
44
|
+
`Head: ${changeSet.headCommit}`,
|
|
45
|
+
`Branch: ${changeSet.branch}`,
|
|
46
|
+
`Changed paths: ${changeSet.changedPaths.length}`,
|
|
47
|
+
...changeSet.changedPaths.map((path) => ` ${path}`),
|
|
48
|
+
`Created: ${changeSet.createdAt}`
|
|
49
|
+
];
|
|
50
|
+
if (changeSet.manifest === undefined) {
|
|
51
|
+
lines.push("Manifest: -");
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
const manifest = changeSet.manifest;
|
|
55
|
+
lines.push("Manifest:", ` Tags: ${manifest.tags.join(", ")}`, ` Deleted paths: ${manifest.deletedPaths.length}`, ...manifest.deletedPaths.map((path) => ` ${path}`), ` Target: ${manifest.targetRef ?? "-"}`, ` Evidence: ${manifest.evidenceRefs.length === 0
|
|
56
|
+
? "-"
|
|
57
|
+
: manifest.evidenceRefs.join(", ")}`);
|
|
58
|
+
}
|
|
59
|
+
return `${lines.join("\n")}\n`;
|
|
60
|
+
}
|