@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.
Files changed (150) hide show
  1. package/README.md +5 -5
  2. package/dist/agent/managedRuntimeEnvironment.js +2 -1
  3. package/dist/cli/commandCatalog.js +251 -13
  4. package/dist/cli/updateOrchestrator.js +8 -0
  5. package/dist/cli/updatePorts.js +76 -22
  6. package/dist/cli.js +264 -20
  7. package/dist/commands/configCommands.js +83 -9
  8. package/dist/commands/controllerCommands.js +103 -0
  9. package/dist/commands/deliveryGuardPreflight.js +30 -0
  10. package/dist/commands/durableJobCommands.js +231 -0
  11. package/dist/commands/executionAuditCommands.js +193 -0
  12. package/dist/commands/grantCommands.js +374 -0
  13. package/dist/commands/projectCommands.js +119 -81
  14. package/dist/commands/releaseCommands.js +444 -0
  15. package/dist/commands/resourcesCommands.js +274 -0
  16. package/dist/commands/sessionCommands.js +104 -0
  17. package/dist/commands/taskActor.js +117 -0
  18. package/dist/commands/taskChangeSetCommands.js +60 -0
  19. package/dist/commands/taskCommands.js +618 -202
  20. package/dist/commands/taskCompletionGate.js +78 -1
  21. package/dist/commands/taskContextCommand.js +33 -6
  22. package/dist/commands/taskInputCommands.js +1 -1
  23. package/dist/commands/taskIntegrationCommands.js +136 -33
  24. package/dist/commands/taskIntegrationQueueCommands.js +228 -0
  25. package/dist/commands/taskNextActionCommand.js +100 -0
  26. package/dist/commands/taskOverlapCommands.js +120 -0
  27. package/dist/commands/taskOverviewCommand.js +36 -8
  28. package/dist/commands/telemetryCommands.js +330 -0
  29. package/dist/commands/workflowCommands.js +415 -0
  30. package/dist/config/yuiConfig.js +62 -0
  31. package/dist/controller/clientRuntime.js +42 -1
  32. package/dist/controller/controller.js +402 -56
  33. package/dist/controller/controllerMain.js +25 -2
  34. package/dist/controller/domainIdentity.js +16 -8
  35. package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
  36. package/dist/controller/handoverCandidate.js +168 -0
  37. package/dist/controller/jobClient.js +102 -0
  38. package/dist/controller/jobControl.js +613 -0
  39. package/dist/controller/jobSupervisor.js +498 -0
  40. package/dist/controller/providerHookRunFence.js +34 -5
  41. package/dist/controller/resourceCleanupLinux.js +18 -9
  42. package/dist/controller/resourceInventoryLinux.js +90 -39
  43. package/dist/controller/runtime.js +165 -15
  44. package/dist/controller/runtimeEventInbox.js +234 -57
  45. package/dist/controller/runtimeEventProcessor.js +297 -58
  46. package/dist/controller/sessionOwnerReconciliation.js +321 -0
  47. package/dist/core/controllerServer.js +416 -27
  48. package/dist/core/controllerTelemetry.js +167 -0
  49. package/dist/doctor/doctor.js +113 -16
  50. package/dist/domain/validation.js +9 -0
  51. package/dist/execution/executionGroup.js +40 -3
  52. package/dist/executor/agentExecutor.js +6 -3
  53. package/dist/executor/effectiveLaunch.js +52 -0
  54. package/dist/executor/executorRegistry.js +50 -0
  55. package/dist/executor/fileRoleLaunchPlanner.js +61 -6
  56. package/dist/grant/capabilityGrant.js +282 -0
  57. package/dist/integration/changeSet.js +16 -3
  58. package/dist/integration/changeSetManifest.js +46 -0
  59. package/dist/integration/gitIntegrationService.js +528 -147
  60. package/dist/integration/integrationAttempt.js +54 -5
  61. package/dist/integration/integrationQueueEntry.js +221 -0
  62. package/dist/integration/integrationQueueService.js +955 -0
  63. package/dist/integration/manifestTags.js +99 -0
  64. package/dist/integration/overlapDiagnostics.js +211 -0
  65. package/dist/job/durableJob.js +449 -0
  66. package/dist/job/jobRunner.js +350 -0
  67. package/dist/lifecycle/exactRunTerminalization.js +24 -2
  68. package/dist/lifecycle/providerErrorClass.js +126 -0
  69. package/dist/message/message.js +16 -3
  70. package/dist/observability/executionAudit.js +545 -0
  71. package/dist/observability/faultClassification.js +160 -0
  72. package/dist/observability/runtimeIdentity.js +367 -0
  73. package/dist/release/fakeReleasePorts.js +55 -0
  74. package/dist/release/releaseHandover.js +475 -0
  75. package/dist/release/releaseIdempotencyStore.js +165 -0
  76. package/dist/release/releaseWorkflow.js +459 -0
  77. package/dist/release/releaseWorkflowEngine.js +688 -0
  78. package/dist/release/releaseWorkflowPorts.js +1720 -0
  79. package/dist/release/runtimeRelease.js +495 -0
  80. package/dist/release/workflowFileLock.js +218 -0
  81. package/dist/repository/gitWorkspace.js +177 -1
  82. package/dist/repository/projectMaintenanceLock.js +315 -0
  83. package/dist/repository/taskWorkspaceCoordinator.js +87 -17
  84. package/dist/repository/taskWorkspacePreparer.js +1091 -517
  85. package/dist/resources/autoResourceGc.js +116 -0
  86. package/dist/resources/liveReferences.js +574 -0
  87. package/dist/resources/resourceDiscovery.js +477 -0
  88. package/dist/resources/resourceGc.js +645 -0
  89. package/dist/resources/resourceRegistrar.js +256 -0
  90. package/dist/resources/resourceRegistry.js +150 -0
  91. package/dist/resources/resourceRegistryStore.js +41 -0
  92. package/dist/resources/resourceTypes.js +42 -0
  93. package/dist/resources/sqliteResourceRegistry.js +111 -0
  94. package/dist/review/reviewConfig.js +10 -0
  95. package/dist/review/reviewFinding.js +240 -0
  96. package/dist/review/reviewFindingLedger.js +545 -0
  97. package/dist/review/reviewOutcomeClassifier.js +61 -0
  98. package/dist/review/reviewRound.js +56 -4
  99. package/dist/run/agentRun.js +80 -4
  100. package/dist/run/providerRetry.js +84 -0
  101. package/dist/run/providerRetryConfig.js +63 -0
  102. package/dist/run/yieldReceipt.js +65 -0
  103. package/dist/runtime/exactControlPlane.js +79 -2
  104. package/dist/runtime/index.js +4 -0
  105. package/dist/runtime/sessionOwnerIdentity.js +269 -0
  106. package/dist/runtime/sessionOwnerRegistry.js +132 -0
  107. package/dist/runtime/sessionReconciliation.js +93 -0
  108. package/dist/runtime/sessionTerminationGuard.js +211 -0
  109. package/dist/runtime/taskRuntimeIsolation.js +13 -0
  110. package/dist/runtime/tmuxAdapters.js +34 -1
  111. package/dist/scheduler/actionability.js +155 -0
  112. package/dist/scheduler/activeRoleRunDelivery.js +14 -5
  113. package/dist/scheduler/activeTaskProgress.js +60 -0
  114. package/dist/scheduler/leaderWakeupProcessor.js +22 -11
  115. package/dist/scheduler/roleRunStall.js +135 -29
  116. package/dist/scheduler/taskExecutionProjection.js +11 -0
  117. package/dist/storage/compatibleTaskStore.js +112 -5
  118. package/dist/storage/migration/productionRegistry.js +736 -1
  119. package/dist/storage/sqliteSchema.js +264 -3
  120. package/dist/storage/sqliteStore.js +487 -13
  121. package/dist/storage/storeRpc.js +21 -0
  122. package/dist/storage/taskStore.js +974 -21
  123. package/dist/storage/upgrade/homeClassification.js +120 -2
  124. package/dist/storage/upgrade/migrationReceipt.js +67 -0
  125. package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
  126. package/dist/storage/upgrade/recordVersions.js +10 -1
  127. package/dist/storage/upgrade/sqliteMigrationTarget.js +58 -6
  128. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
  129. package/dist/storage/upgrade/sqliteStateMigration.js +258 -2
  130. package/dist/storage/upgrade/upgradeOrchestrator.js +482 -16
  131. package/dist/task/deliveryGuard.js +226 -0
  132. package/dist/task/nextAction.js +738 -0
  133. package/dist/task/repairWave.js +137 -0
  134. package/dist/task/taskRecordReference.js +6 -1
  135. package/dist/telemetry/sqliteTelemetryStore.js +387 -0
  136. package/dist/telemetry/telemetryCompaction.js +251 -0
  137. package/dist/telemetry/telemetryConfig.js +64 -0
  138. package/dist/telemetry/telemetryRouter.js +32 -0
  139. package/dist/telemetry/telemetryStore.js +19 -0
  140. package/dist/telemetry/telemetryWiring.js +33 -0
  141. package/dist/tmux/tmuxManager.js +20 -1
  142. package/dist/tmux/tmuxSocketEndpoint.js +20 -0
  143. package/dist/verification/gateArtifact.js +216 -0
  144. package/dist/verification/gateArtifactStore.js +87 -0
  145. package/dist/verification/verificationGateService.js +414 -0
  146. package/dist/verification/verificationPlan.js +308 -0
  147. package/dist/workspace/gitChangeSetCapture.js +12 -2
  148. package/dist/workspace/workItemChangeSetManager.js +60 -3
  149. package/package.json +1 -1
  150. 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
+ }