@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,168 @@
1
+ /**
2
+ * Controller handover candidate prelude (Issue 02).
3
+ *
4
+ * A handover candidate is the new release's Controller process started while
5
+ * the old Controller still owns the Home. It does NOT bind the main socket or
6
+ * run the scheduler: it proves the new release starts, publishes its identity
7
+ * receipt, and waits until the old owner exits AND the active release pointer
8
+ * names its release. Only then does it promote itself by starting the full
9
+ * Controller runtime in-process.
10
+ *
11
+ * If the old owner stays live after the commit (a stuck Controller), the
12
+ * candidate stays read-only and reports `dualOwner: true` in its identity
13
+ * receipt; it never writes while another owner is live.
14
+ */
15
+ import { fileURLToPath } from "node:url";
16
+ import { buildRuntimeIdentityReceipt } from "../core/controllerServer.js";
17
+ import { resolveStoreWorkerEnabledForHome } from "../storage/storeRpc.js";
18
+ import { resolveTaskStoreBackendForHome } from "../storage/sqliteStore.js";
19
+ import { detectRunningRelease, isOwnerLive, readActiveReleasePointer, readHandoverFence, removeCandidateDiscovery, removeHandoverFence, writeCandidateDiscovery, writeHandoverFence, writeHandoverReceipt, writeRuntimeIdentity } from "../release/runtimeRelease.js";
20
+ import { startFileTaskControllerRuntime } from "./runtime.js";
21
+ import { readLinuxProcessStartIdentity } from "./domainIdentity.js";
22
+ export const CONTROLLER_CANDIDATE_ENV = "YUI_CONTROLLER_CANDIDATE";
23
+ export const CONTROLLER_HANDOVER_ID_ENV = "YUI_CONTROLLER_HANDOVER_ID";
24
+ const DEFAULT_POLL_INTERVAL_MS = 100;
25
+ /**
26
+ * The grace a committed candidate waits for the old owner to exit before it
27
+ * latches `dualOwner: true`. This is the single authoritative old-owner exit
28
+ * grace: the activator trusts the candidate's latched signal and does not add
29
+ * a second independent grace (see `DEFAULT_DUAL_OWNER_GRACE_MS` in
30
+ * `releaseHandover.ts`, which is only an optional confirmation debounce).
31
+ */
32
+ export const DEFAULT_DUAL_OWNER_GRACE_MS = 30_000;
33
+ /** Reads the candidate configuration from the process environment. */
34
+ export function handoverCandidateFromEnvironment(environment) {
35
+ if (environment[CONTROLLER_CANDIDATE_ENV] !== "1")
36
+ return null;
37
+ const handoverId = environment[CONTROLLER_HANDOVER_ID_ENV];
38
+ if (typeof handoverId !== "string"
39
+ || handoverId.length === 0
40
+ || handoverId.length > 128) {
41
+ throw new Error(`Handover candidate requires ${CONTROLLER_HANDOVER_ID_ENV} with a valid handover id.`);
42
+ }
43
+ return { handoverId };
44
+ }
45
+ /**
46
+ * Runs the candidate prelude and, on promotion, the full Controller runtime.
47
+ * Resolves when the promoted Controller closes or the candidate aborts.
48
+ */
49
+ export async function runHandoverCandidate(home, handoverId, options = {}) {
50
+ const environment = options.environment ?? process.env;
51
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
52
+ const dualOwnerGraceMs = options.dualOwnerGraceMs ?? DEFAULT_DUAL_OWNER_GRACE_MS;
53
+ const now = options.now ?? (() => new Date());
54
+ const release = detectRunningRelease(fileURLToPath(import.meta.url));
55
+ const startIdentity = readLinuxProcessStartIdentity(process.pid);
56
+ if (startIdentity === undefined) {
57
+ throw new Error(`Cannot read process start identity for handover candidate PID ${process.pid}.`);
58
+ }
59
+ const fence = readHandoverFence(home);
60
+ if (fence === null || fence.handoverId !== handoverId) {
61
+ throw new Error(`Handover fence for ${handoverId} is missing or does not match; aborting candidate.`);
62
+ }
63
+ // A candidate only starts from the `fenced` phase. If the activator already
64
+ // rolled the handover back (fence written as `rolled-back`, old Controller
65
+ // restored), a slow-starting candidate must not resurrect the fence to
66
+ // `candidate-ready`: that would leak the candidate process and could lead a
67
+ // later `release activate` recovery into a split-brain commit.
68
+ if (fence.phase !== "fenced") {
69
+ removeCandidateDiscovery(home);
70
+ return {
71
+ outcome: "aborted",
72
+ reason: `Handover fence is in phase '${fence.phase}', not 'fenced'; the handover was rolled back.`
73
+ };
74
+ }
75
+ // Publish the candidate identity so the activator can read back build ID,
76
+ // backend, and worker state before committing the handover.
77
+ writeCandidateDiscovery(home, { pid: process.pid, processStartIdentity: startIdentity });
78
+ writeRuntimeIdentity(home, buildRuntimeIdentityReceipt({
79
+ home,
80
+ release,
81
+ storageBackend: resolveTaskStoreBackendForHome(home, environment),
82
+ workerEnabled: resolveStoreWorkerEnabledForHome(home, environment),
83
+ processStartIdentity: startIdentity,
84
+ mode: "candidate",
85
+ dualOwner: false
86
+ }));
87
+ advanceFence(home, fence, "candidate-ready", now().toISOString());
88
+ const committedAt = Date.now();
89
+ let dualOwnerReported = false;
90
+ for (;;) {
91
+ const current = readHandoverFence(home);
92
+ if (current === null || current.handoverId !== handoverId) {
93
+ removeCandidateDiscovery(home);
94
+ return {
95
+ outcome: "aborted",
96
+ reason: "Handover fence disappeared; the handover was rolled back."
97
+ };
98
+ }
99
+ if (current.phase === "rolled-back") {
100
+ removeCandidateDiscovery(home);
101
+ return {
102
+ outcome: "aborted",
103
+ reason: "Handover was rolled back while the candidate was polling."
104
+ };
105
+ }
106
+ const oldDead = !isOwnerLive(current.old);
107
+ const pointer = readActiveReleasePointer(home);
108
+ const pointerSwitched = pointer !== null && pointer.releaseId === current.toReleaseId;
109
+ if (oldDead && pointerSwitched) {
110
+ return await promote(home, current, environment, now);
111
+ }
112
+ if (current.phase === "committed"
113
+ && !oldDead
114
+ && Date.now() - committedAt > dualOwnerGraceMs
115
+ && !dualOwnerReported) {
116
+ // The old Controller was told to exit but is still live. Stay read-only
117
+ // and make the dual-owner condition visible; never write concurrently.
118
+ dualOwnerReported = true;
119
+ writeRuntimeIdentity(home, buildRuntimeIdentityReceipt({
120
+ home,
121
+ release,
122
+ storageBackend: resolveTaskStoreBackendForHome(home, environment),
123
+ workerEnabled: resolveStoreWorkerEnabledForHome(home, environment),
124
+ processStartIdentity: startIdentity,
125
+ mode: "candidate",
126
+ dualOwner: true
127
+ }));
128
+ }
129
+ await delay(pollIntervalMs);
130
+ }
131
+ }
132
+ async function promote(home, fence, environment, now) {
133
+ removeCandidateDiscovery(home);
134
+ const controller = await startFileTaskControllerRuntime(home, { environment });
135
+ // The promoted Controller server has already published its primary identity
136
+ // receipt. Complete the handover record and release the fence.
137
+ writeHandoverReceipt(home, Object.freeze({
138
+ schemaVersion: 1,
139
+ handoverId: fence.handoverId,
140
+ outcome: "completed",
141
+ old: fence.old,
142
+ candidate: fence.candidate,
143
+ previousReleaseId: fence.fromReleaseId,
144
+ activatedReleaseId: fence.toReleaseId,
145
+ startedAt: fence.createdAt,
146
+ completedAt: now().toISOString()
147
+ }));
148
+ removeHandoverFence(home);
149
+ const stop = () => {
150
+ void controller.close().catch(() => undefined);
151
+ };
152
+ process.once("SIGINT", stop);
153
+ process.once("SIGTERM", stop);
154
+ try {
155
+ await controller.closed;
156
+ }
157
+ finally {
158
+ process.removeListener("SIGINT", stop);
159
+ process.removeListener("SIGTERM", stop);
160
+ }
161
+ return { outcome: "promoted", reason: "Candidate promoted and Controller closed." };
162
+ }
163
+ function advanceFence(home, fence, phase, updatedAt) {
164
+ writeHandoverFence(home, Object.freeze({ ...fence, phase, updatedAt }));
165
+ }
166
+ function delay(ms) {
167
+ return new Promise((resolve) => setTimeout(resolve, ms));
168
+ }
@@ -0,0 +1,102 @@
1
+ import { resolveJobCaller } from "../commands/taskActor.js";
2
+ import { callFileTaskController } from "./clientRuntime.js";
3
+ export async function startDurableJob(home, params, clientOptions = {}) {
4
+ // rr6/f2: Route through callFileTaskController so an Integration-issued
5
+ // job.start starts a stopped per-Home Controller on demand, mirroring the
6
+ // `job start` CLI. A bare callController only reads discovery and fails.
7
+ const result = await callFileTaskController(home, "job.start", params, clientOptions);
8
+ return parseStartResult(result);
9
+ }
10
+ export async function getDurableJob(home, taskId, jobId, clientOptions = {}) {
11
+ const result = await callFileTaskController(home, "job.get", { taskId, jobId }, clientOptions);
12
+ return parseJobResult(result);
13
+ }
14
+ export async function acknowledgeDurableJob(home, taskId, jobId, caller, clientOptions = {}) {
15
+ const result = await callFileTaskController(home, "job.acknowledge", {
16
+ taskId,
17
+ jobId,
18
+ caller
19
+ }, clientOptions);
20
+ if (typeof result !== "object" || result === null || Array.isArray(result)
21
+ || typeof result.acknowledged !== "boolean") {
22
+ throw new Error("Controller job.acknowledge returned an invalid result.");
23
+ }
24
+ return {
25
+ job: parseJobResult(result),
26
+ acknowledged: result.acknowledged
27
+ };
28
+ }
29
+ export async function cancelDurableJob(home, taskId, jobId, caller, clientOptions = {}) {
30
+ const result = await callFileTaskController(home, "job.cancel", { taskId, jobId, caller }, clientOptions);
31
+ if (typeof result !== "object" || result === null || Array.isArray(result)
32
+ || typeof result.cancelRequested !== "boolean") {
33
+ throw new Error("Controller job.cancel returned an invalid result.");
34
+ }
35
+ return {
36
+ job: parseJobResult(result),
37
+ cancelRequested: result.cancelRequested
38
+ };
39
+ }
40
+ function parseStartResult(value) {
41
+ if (typeof value !== "object" || value === null || Array.isArray(value)
42
+ || typeof value.created !== "boolean") {
43
+ throw new Error("Controller job.start returned an invalid result.");
44
+ }
45
+ return {
46
+ job: parseJobResult(value),
47
+ created: value.created
48
+ };
49
+ }
50
+ function parseJobResult(value) {
51
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
52
+ throw new Error("Controller returned an invalid DurableJob result.");
53
+ }
54
+ const job = value.job;
55
+ if (typeof job !== "object" || job === null || Array.isArray(job)) {
56
+ throw new Error("Controller returned an invalid DurableJob record.");
57
+ }
58
+ return job;
59
+ }
60
+ /**
61
+ * Production IntegrationJobPort: Integration checks ask the Controller socket
62
+ * for their DurableJob. The Controller owns the detached runner, so a Leader
63
+ * exit or Session replacement never kills a running check.
64
+ *
65
+ * rr6/f2: The port routes through callFileTaskController, so an Integration
66
+ * operation that issues a job.* RPC (start/continue/abort, and job-backed Task
67
+ * completion) starts a stopped per-Home Controller on demand. Read-only
68
+ * list/show paths do not use this port and stay lean.
69
+ */
70
+ export function createControllerIntegrationJobPort(home, clientOptions = {}) {
71
+ const { store, ...controllerOptions } = clientOptions;
72
+ return {
73
+ async startCheckJob(input) {
74
+ const owner = {
75
+ kind: "integration-attempt",
76
+ integrationAttemptId: input.integrationId
77
+ };
78
+ // rr8/rr12: Bind the Integration-issued job to the caller's managed
79
+ // identity. A user-scope caller carries a verified Leader assertion.
80
+ const caller = resolveJobCaller(controllerOptions.environment, input.taskId, store);
81
+ const { job } = await startDurableJob(home, {
82
+ taskId: input.taskId,
83
+ owner,
84
+ projectId: input.projectId,
85
+ head: input.head,
86
+ workspace: input.workspace,
87
+ env: input.env,
88
+ steps: input.steps,
89
+ caller
90
+ }, controllerOptions);
91
+ return job;
92
+ },
93
+ async getJob(taskId, jobId) {
94
+ return getDurableJob(home, taskId, jobId, controllerOptions);
95
+ },
96
+ async cancelJob(taskId, jobId) {
97
+ // rr8/rr12: Bind the cancel request to the caller's managed identity.
98
+ const caller = resolveJobCaller(controllerOptions.environment, taskId, store);
99
+ await cancelDurableJob(home, taskId, jobId, caller, controllerOptions);
100
+ }
101
+ };
102
+ }