@zq-silk/yui 0.16.0 → 0.16.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/ARCHITECTURE.md +12 -0
- package/ARCHITECTURE.zh-CN.md +8 -0
- package/README.md +3 -0
- package/dist/agent/managedRuntimeEnvironment.js +0 -4
- package/dist/agentRun/agentRun.js +7 -17
- package/dist/cli/agentConfigurationPicker.js +24 -21
- package/dist/cli/commandCatalog.js +18 -10
- package/dist/cli/roleWizard.js +41 -30
- package/dist/cli/updateCommand.js +35 -13
- package/dist/cli/updateOrchestrator.js +73 -33
- package/dist/cli/updatePorts.js +51 -20
- package/dist/cli.js +35 -26
- package/dist/commands/controllerCommands.js +1 -1
- package/dist/commands/executionAuditCommands.js +2 -2
- package/dist/commands/projectCommands.js +1 -1
- package/dist/commands/releaseCommands.js +9 -38
- package/dist/commands/taskCommands.js +84 -81
- package/dist/commands/taskCompletionGate.js +0 -117
- package/dist/commands/taskRoleRuntimeStatus.js +1 -1
- package/dist/commands/taskUpstreamCommands.js +93 -54
- package/dist/context/runContextPack.js +30 -54
- package/dist/context/runInputContract.js +9 -0
- package/dist/context/sessionBootstrapManifest.js +11 -19
- package/dist/controller/agentHostObservation.js +4 -2
- package/dist/controller/controller.js +8 -6
- package/dist/controller/fileSchedulerStoreAdapter.js +85 -18
- package/dist/controller/globalInputDelivery.js +28 -7
- package/dist/controller/globalRuntimeAttention.js +34 -0
- package/dist/controller/operatorNotification.js +31 -0
- package/dist/controller/providerRetryAdmission.js +3 -1
- package/dist/controller/providerRetryDelivery.js +10 -3
- package/dist/controller/runtime.js +1 -27
- package/dist/controller/sessionOwnerReconciliation.js +42 -24
- package/dist/controller/structuredProviderObservation.js +27 -0
- package/dist/controller/updateReconciliation.js +72 -20
- package/dist/doctor/doctor.js +2 -1
- package/dist/executor/agentAdapter.js +28 -96
- package/dist/executor/agentConfigurationCatalog.js +13 -68
- package/dist/executor/agentConfigurationFields.js +120 -0
- package/dist/executor/agentConfigurationProbe.js +55 -80
- package/dist/executor/agentExecutor.js +5 -2
- package/dist/executor/executorRegistry.js +0 -9
- package/dist/executor/fileRoleLaunchPlanner.js +12 -11
- package/dist/interaction/operatorPresentation.js +2 -1
- package/dist/message/messageContinuation.js +8 -9
- package/dist/observability/executionAudit.js +1 -10
- package/dist/observability/runtimeIdentity.js +0 -23
- package/dist/output/agentConfigurationPresentation.js +4 -2
- package/dist/release/releaseHandover.js +2 -2
- package/dist/release/releaseIdempotencyStore.js +0 -23
- package/dist/release/releaseWorkflowPorts.js +8 -4
- package/dist/repository/gitWorkspace.js +66 -19
- package/dist/repository/taskWorkspacePreparer.js +75 -68
- package/dist/resources/resourceRegistry.js +90 -44
- package/dist/resources/sqliteResourceRegistry.js +3 -5
- package/dist/runtime/acpSession.js +21 -5
- package/dist/runtime/agentEndpoint.js +4 -1
- package/dist/runtime/agentHost.js +22 -62
- package/dist/runtime/agentHostCleanup.js +85 -0
- package/dist/runtime/agentHostProtocol.js +1 -1
- package/dist/runtime/builtinAgentDrivers.js +46 -119
- package/dist/runtime/builtinTranscriptObserver.js +10 -6
- package/dist/runtime/codexAppServerRuntime.js +55 -89
- package/dist/runtime/jsonLineChannel.js +35 -7
- package/dist/runtime/providerContinuationReconciliationService.js +42 -3
- package/dist/runtime/providerRetry.js +15 -0
- package/dist/runtime/providerRuntimeReconciler.js +12 -2
- package/dist/runtime/structuredProviderHost.js +106 -81
- package/dist/scheduler/activeRoleRunDelivery.js +5 -1
- package/dist/scheduler/operatorInputNotificationProcessor.js +7 -17
- package/dist/storage/upgrade/upgradeOrchestrator.js +0 -10
- package/dist/task/nextAction.js +33 -23
- package/dist/task/task.js +2 -1
- package/dist/web/assets/assetManifest.js +2 -0
- package/dist/web/assets/client/app.js +72 -3
- package/dist/web/assets/client/components.js +2 -1
- package/dist/web/assets/client/i18n.js +2 -0
- package/dist/web/assets/client/taskSummary.js +345 -0
- package/dist/web/assets/client/taskSurface.js +44 -26
- package/dist/web/assets/client/view.js +33 -0
- package/dist/web/assets/shell.js +1 -0
- package/dist/web/assets/styles/cards.js +21 -0
- package/dist/web/webServer.js +39 -5
- package/dist/web/webSessions.js +165 -0
- package/dist/web/webSnapshot.js +23 -0
- package/dist/web/webTaskSurface.js +23 -0
- package/docs/managed-turn-and-session-runtime.md +31 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +23 -0
- package/docs/observability/README.md +47 -0
- package/docs/observability/README.zh-CN.md +37 -0
- package/docs/project-refresh.md +9 -0
- package/docs/project-refresh.zh-CN.md +8 -0
- package/docs/provider-retry.md +35 -0
- package/docs/release-workflow.md +139 -20
- package/docs/release-workflow.zh-CN.md +115 -14
- package/docs/roles-and-configuration.md +30 -0
- package/docs/roles-and-configuration.zh-CN.md +20 -0
- package/docs/task-delivery.md +109 -0
- package/docs/task-delivery.zh-CN.md +80 -0
- package/docs/task-discovery.md +9 -0
- package/docs/task-discovery.zh-CN.md +6 -0
- package/docs/testing/verification-levels.md +33 -12
- package/docs/testing/verification-levels.zh-CN.md +28 -8
- package/i18n/README.zh-CN.md +2 -0
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +11 -0
- package/skills/yui-leader/references/execution.md +22 -0
- package/skills/yui-leader/references/planning.md +6 -2
- package/skills/yui-operator/SKILL.md +45 -13
- package/skills/yui-operator/references/task-delivery.md +99 -0
- package/skills/yui-runtime/SKILL.md +5 -3
- package/skills/yui-runtime/references/publication.md +56 -5
- package/skills/yui-runtime/references/recovery.md +12 -2
- package/dist/runtime/agentHostCompatibility.js +0 -127
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
* then activated, any required storage migration is applied, and the result is
|
|
7
7
|
* verified before the Controller is restarted.
|
|
8
8
|
*/
|
|
9
|
+
import { UpdateControllerReconciliationError } from "../controller/updateReconciliation.js";
|
|
9
10
|
export function runUpdate(ports, options) {
|
|
10
11
|
let staged;
|
|
11
12
|
try {
|
|
12
|
-
staged = ports.stage();
|
|
13
|
+
staged = ports.stage(options.version);
|
|
13
14
|
}
|
|
14
15
|
catch (error) {
|
|
15
16
|
return {
|
|
@@ -33,7 +34,9 @@ export function runUpdate(ports, options) {
|
|
|
33
34
|
cleanupWarning = `Staging cleanup could not be completed: ${messageOf(error)}`;
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
|
-
return cleanupWarning === undefined ? result : {
|
|
37
|
+
return cleanupWarning === undefined ? result : {
|
|
38
|
+
...result, cleanupWarning: [result.cleanupWarning, cleanupWarning].filter(Boolean).join(" ")
|
|
39
|
+
};
|
|
37
40
|
}
|
|
38
41
|
function runStagedUpdate(ports, staged, home) {
|
|
39
42
|
let preflight;
|
|
@@ -97,38 +100,70 @@ function runStagedUpdate(ports, staged, home) {
|
|
|
97
100
|
version: staged.version
|
|
98
101
|
};
|
|
99
102
|
}
|
|
103
|
+
let result;
|
|
104
|
+
let cleanupWarning;
|
|
100
105
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
// An older Controller can finish a launch between preflight and drain.
|
|
105
|
-
// Recheck after its exact stop, before changing the install or storage.
|
|
106
|
-
let fencedPreflight;
|
|
106
|
+
result = reconcileAndUpdate(ports, staged, home);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
107
109
|
try {
|
|
108
|
-
|
|
110
|
+
releaseHandover?.();
|
|
109
111
|
}
|
|
110
112
|
catch (error) {
|
|
111
|
-
|
|
112
|
-
outcome: "aborted", phase: "preflight",
|
|
113
|
-
message: `Quiesced compatibility preflight failed: ${messageOf(error)}`,
|
|
114
|
-
action: "The install and storage are unchanged; inspect the compatibility failure before retrying.",
|
|
115
|
-
recoverable: true, version: staged.version
|
|
116
|
-
});
|
|
113
|
+
cleanupWarning = `Controller handover lock release failed for ${home}: ${messageOf(error)}`;
|
|
117
114
|
}
|
|
118
|
-
if (fencedPreflight.status === "blocked") {
|
|
119
|
-
return restoreControllerOrReport(ports, home, captured.lifecycle, {
|
|
120
|
-
outcome: "aborted", phase: "preflight",
|
|
121
|
-
message: fencedPreflight.message, action: fencedPreflight.action,
|
|
122
|
-
recoverable: true, version: staged.version,
|
|
123
|
-
...(fencedPreflight.blockers === undefined ? {} : { blockers: fencedPreflight.blockers }),
|
|
124
|
-
...(fencedPreflight.sceneUnchanged === true ? { sceneUnchanged: true } : {})
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
return activateAndVerify(ports, staged, home, captured.lifecycle, fencedPreflight);
|
|
128
115
|
}
|
|
129
|
-
|
|
130
|
-
|
|
116
|
+
return cleanupWarning === undefined ? result : { ...result, cleanupWarning };
|
|
117
|
+
}
|
|
118
|
+
function reconcileAndUpdate(ports, staged, home) {
|
|
119
|
+
let reconciliation;
|
|
120
|
+
try {
|
|
121
|
+
reconciliation = ports.reconcileController?.(home);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
const evidence = error instanceof UpdateControllerReconciliationError ? error.result : undefined;
|
|
125
|
+
const unknown = evidence === undefined || evidence.observationError !== undefined
|
|
126
|
+
|| evidence.lockReleaseError !== undefined || evidence.attempts.some(attempt => attempt.outcome === "unknown");
|
|
127
|
+
return {
|
|
128
|
+
outcome: "aborted", phase: "coordination", version: staged.version,
|
|
129
|
+
message: `Controller reconciliation failed: ${messageOf(error)}`,
|
|
130
|
+
action: "Inspect the reported current-Home targets and partial effects; binary activation was not attempted.",
|
|
131
|
+
recoverable: !unknown,
|
|
132
|
+
...(evidence === undefined ? {} : { controllerReconciliation: evidence }),
|
|
133
|
+
...(unknown ? { controllerOwnershipUnknown: true } : {})
|
|
134
|
+
};
|
|
131
135
|
}
|
|
136
|
+
const coordinated = runCoordinatedUpdate(ports, staged, home);
|
|
137
|
+
return reconciliation === undefined ? coordinated : { ...coordinated, controllerReconciliation: reconciliation };
|
|
138
|
+
}
|
|
139
|
+
function runCoordinatedUpdate(ports, staged, home) {
|
|
140
|
+
const captured = captureControllerLifecycle(ports, staged.version, home);
|
|
141
|
+
if ("outcome" in captured)
|
|
142
|
+
return captured;
|
|
143
|
+
// Storage can change before the Controller drains. Recheck after its
|
|
144
|
+
// exact stop, before changing the install or storage. Its sceneUnchanged
|
|
145
|
+
// verdict does not describe the preceding runtime cleanup/stop/restore.
|
|
146
|
+
let fencedPreflight;
|
|
147
|
+
try {
|
|
148
|
+
fencedPreflight = ports.preflight(staged, home);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
return restoreControllerOrReport(ports, home, captured.lifecycle, {
|
|
152
|
+
outcome: "aborted", phase: "preflight",
|
|
153
|
+
message: `Quiesced storage preflight failed: ${messageOf(error)}`,
|
|
154
|
+
action: "The install and storage are unchanged; inspect the storage failure before retrying.",
|
|
155
|
+
recoverable: true, version: staged.version
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (fencedPreflight.status === "blocked") {
|
|
159
|
+
return restoreControllerOrReport(ports, home, captured.lifecycle, {
|
|
160
|
+
outcome: "aborted", phase: "preflight",
|
|
161
|
+
message: fencedPreflight.message, action: fencedPreflight.action,
|
|
162
|
+
recoverable: true, version: staged.version,
|
|
163
|
+
...(fencedPreflight.blockers === undefined ? {} : { blockers: fencedPreflight.blockers })
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return activateAndVerify(ports, staged, home, captured.lifecycle, fencedPreflight);
|
|
132
167
|
}
|
|
133
168
|
function activateAndVerify(ports, staged, home, lifecycle, preflight) {
|
|
134
169
|
try {
|
|
@@ -256,7 +291,8 @@ function captureControllerLifecycle(ports, version, home) {
|
|
|
256
291
|
phase: "preflight",
|
|
257
292
|
message: `Controller status could not be verified: ${messageOf(error)}`,
|
|
258
293
|
action: "Inspect Controller ownership and retry after its status is known.",
|
|
259
|
-
recoverable:
|
|
294
|
+
recoverable: false,
|
|
295
|
+
controllerOwnershipUnknown: true,
|
|
260
296
|
version
|
|
261
297
|
};
|
|
262
298
|
}
|
|
@@ -279,7 +315,8 @@ function captureControllerLifecycle(ports, version, home) {
|
|
|
279
315
|
phase: "preflight",
|
|
280
316
|
message: `Controller stop/drain failed: ${messageOf(error)}`,
|
|
281
317
|
action: "Inspect the captured Controller before retrying; binary activation was not attempted.",
|
|
282
|
-
recoverable:
|
|
318
|
+
recoverable: false,
|
|
319
|
+
controllerOwnershipUnknown: true,
|
|
283
320
|
version
|
|
284
321
|
};
|
|
285
322
|
}
|
|
@@ -300,7 +337,8 @@ function malformedControllerResult(version, message) {
|
|
|
300
337
|
phase: "preflight",
|
|
301
338
|
message,
|
|
302
339
|
action: "Refusing an unfenced Controller handoff; inspect ownership and retry.",
|
|
303
|
-
recoverable:
|
|
340
|
+
recoverable: false,
|
|
341
|
+
controllerOwnershipUnknown: true,
|
|
304
342
|
version
|
|
305
343
|
};
|
|
306
344
|
}
|
|
@@ -309,14 +347,16 @@ function restoreControllerOrReport(ports, home, lifecycle, failure) {
|
|
|
309
347
|
return failure;
|
|
310
348
|
try {
|
|
311
349
|
ports.restoreController(home, lifecycle.identity);
|
|
312
|
-
return failure;
|
|
350
|
+
return { ...failure, controllerRestore: { identity: lifecycle.identity, outcome: "restored" } };
|
|
313
351
|
}
|
|
314
352
|
catch (error) {
|
|
315
353
|
return {
|
|
316
354
|
...failure,
|
|
317
355
|
message: `${failure.message} Captured Controller restore failed: ${messageOf(error)}.`,
|
|
318
|
-
action: `${failure.action}
|
|
319
|
-
recoverable: false
|
|
356
|
+
action: `${failure.action} Controller liveness after the failed restore is unknown; inspect its exact identity before further action.`,
|
|
357
|
+
recoverable: false,
|
|
358
|
+
controllerOwnershipUnknown: true,
|
|
359
|
+
controllerRestore: { identity: lifecycle.identity, outcome: "unknown", error: messageOf(error) }
|
|
320
360
|
};
|
|
321
361
|
}
|
|
322
362
|
}
|
package/dist/cli/updatePorts.js
CHANGED
|
@@ -34,6 +34,7 @@ import { STORAGE_DOCTOR_CHECK_NAMES } from "../doctor/doctor.js";
|
|
|
34
34
|
import { acquireHandoverLock } from "../release/runtimeRelease.js";
|
|
35
35
|
import { updateStagingRoot } from "../storage/homeLayout.js";
|
|
36
36
|
import { resolveYuiHome } from "../storage/taskStore.js";
|
|
37
|
+
import { UpdateControllerReconciliationError } from "../controller/updateReconciliation.js";
|
|
37
38
|
const PACKAGE_NAME = "@zq-silk/yui";
|
|
38
39
|
const PACKAGE_SPEC = `${PACKAGE_NAME}@latest`;
|
|
39
40
|
function resolveExecutable(command, environmentPath) {
|
|
@@ -90,20 +91,20 @@ export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot =
|
|
|
90
91
|
beginControllerHandover(home) {
|
|
91
92
|
return acquireHandoverLock(home).release;
|
|
92
93
|
},
|
|
93
|
-
stage(
|
|
94
|
+
stage(requestedVersion) {
|
|
94
95
|
// A caller that names a version (the release workflow, which freezes the
|
|
95
96
|
// exact version in its plan) installs THAT version — never a moving
|
|
96
97
|
// `latest` that could resolve to a different build than the one the
|
|
97
98
|
// plan authorized. A non-concrete value fails closed rather than being
|
|
98
99
|
// interpolated into an install spec. An omitted version keeps the
|
|
99
100
|
// interactive `yui update` behavior of staging latest.
|
|
100
|
-
const spec =
|
|
101
|
+
const spec = requestedVersion === undefined
|
|
101
102
|
? PACKAGE_SPEC
|
|
102
|
-
: isConcreteVersion(
|
|
103
|
-
? `${PACKAGE_NAME}@${
|
|
103
|
+
: isConcreteVersion(requestedVersion)
|
|
104
|
+
? `${PACKAGE_NAME}@${requestedVersion.trim()}`
|
|
104
105
|
: null;
|
|
105
106
|
if (spec === null) {
|
|
106
|
-
throw runtimeError(`Refusing to stage a non-concrete version (${String(
|
|
107
|
+
throw runtimeError(`Refusing to stage a non-concrete version (${String(requestedVersion)}): only an exact `
|
|
107
108
|
+ "major.minor.patch version can be pinned for an update.");
|
|
108
109
|
}
|
|
109
110
|
// The Home staging parent may not exist yet on a Home that has never
|
|
@@ -126,6 +127,10 @@ export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot =
|
|
|
126
127
|
+ "nor `yui --json version` returned a concrete version). Refusing to proceed with a "
|
|
127
128
|
+ "`@latest` fallback that could promote a different build than the one preflighted.");
|
|
128
129
|
}
|
|
130
|
+
if (requestedVersion !== undefined && version !== requestedVersion.trim()) {
|
|
131
|
+
throw runtimeError(`Requested Yui ${requestedVersion.trim()}, but staged Yui ${version}. `
|
|
132
|
+
+ "Refusing to activate a different release; the install and storage are unchanged.");
|
|
133
|
+
}
|
|
129
134
|
// Successful staging transfers cleanup ownership to runUpdate's finally
|
|
130
135
|
// block. Every assertion, spawn, npm/network, or version-resolution
|
|
131
136
|
// failure before that handoff removes the throwaway prefix here.
|
|
@@ -196,10 +201,9 @@ export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot =
|
|
|
196
201
|
+ `but the staged/verified artifact was ${staged.version}. Refusing to trust a `
|
|
197
202
|
+ "different build than the one that passed preflight.");
|
|
198
203
|
}
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
// the update cannot strand a live Session.
|
|
204
|
+
// Retarget current Manifest-referenced wrappers to the activated
|
|
205
|
+
// installation before the replacement Controller starts. This relocates
|
|
206
|
+
// the current shell form; it does not convert historical Session wrappers.
|
|
203
207
|
const sessionCliRefresh = run(activeBinary, ["--json", "internal", "session-cli-refresh"], { cwd: process.cwd(), env: { ...environment, YUI_HOME: home }, shell: false });
|
|
204
208
|
assertSpawnOk(sessionCliRefresh, "refresh managed Session CLI wrappers");
|
|
205
209
|
// Retain the exact path used by both doctor and version verification. The
|
|
@@ -214,13 +218,15 @@ export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot =
|
|
|
214
218
|
},
|
|
215
219
|
// `runUpdate` is intentionally synchronous because the npm/staged-binary
|
|
216
220
|
// ports use spawnSync. A short-lived child owns the async, exactly fenced
|
|
217
|
-
// reconciliation
|
|
221
|
+
// reconciliation explicitly; lifecycle capture uses only structured live
|
|
218
222
|
// Controller commands. Tests can replace these seams with deterministic
|
|
219
223
|
// fakes without touching a real Controller.
|
|
220
224
|
controllerStatus(home) {
|
|
221
|
-
reconcileControllerResourcesForUpdate(home, environment, spawn);
|
|
222
225
|
return readControllerLifecycle(home, environment, spawn);
|
|
223
226
|
},
|
|
227
|
+
reconcileController(home) {
|
|
228
|
+
return reconcileControllerResourcesForUpdate(home, environment, spawn);
|
|
229
|
+
},
|
|
224
230
|
stopController(home, expectedPid) {
|
|
225
231
|
return stopControllerForUpdate(home, expectedPid, environment, spawn);
|
|
226
232
|
},
|
|
@@ -355,6 +361,7 @@ function stopReplacementControllerForUpdate(home, expectedPid, environment, spaw
|
|
|
355
361
|
function runSchemaIndependentControllerStop(home, environment, spawn, expectedPid) {
|
|
356
362
|
const helper = [
|
|
357
363
|
"const values = process.argv.slice(1);",
|
|
364
|
+
"const handoverOwnerPid = Number(values.pop());",
|
|
358
365
|
"const expectedPid = values.length === 3 ? Number(values.pop()) : undefined;",
|
|
359
366
|
"const home = values.pop();",
|
|
360
367
|
"const runtimeModule = values.pop();",
|
|
@@ -362,6 +369,7 @@ function runSchemaIndependentControllerStop(home, environment, spawn, expectedPi
|
|
|
362
369
|
" const { stopFileTaskController } = await import(runtimeModule);",
|
|
363
370
|
" const data = await stopFileTaskController(home, {",
|
|
364
371
|
" environment: process.env,",
|
|
372
|
+
" handoverOwnerPid,",
|
|
365
373
|
" ...(expectedPid === undefined ? {} : { expectedPid })",
|
|
366
374
|
" });",
|
|
367
375
|
" process.stdout.write(JSON.stringify({ ok: true, data }));",
|
|
@@ -377,7 +385,10 @@ function runSchemaIndependentControllerStop(home, environment, spawn, expectedPi
|
|
|
377
385
|
helper,
|
|
378
386
|
UPDATE_CLIENT_RUNTIME_PATH,
|
|
379
387
|
home,
|
|
380
|
-
...(expectedPid === undefined ? [] : [String(expectedPid)])
|
|
388
|
+
...(expectedPid === undefined ? [] : [String(expectedPid)]),
|
|
389
|
+
// The runtime validates the existing lock owner; the child must identify
|
|
390
|
+
// its owning updater instead of waiting on the parent's lock as foreign.
|
|
391
|
+
String(process.pid)
|
|
381
392
|
], { cwd: process.cwd(), env: { ...environment, YUI_HOME: home }, shell: false });
|
|
382
393
|
if (result.error !== undefined || result.status !== 0) {
|
|
383
394
|
const detail = result.stderr.toString("utf8").trim();
|
|
@@ -491,10 +502,11 @@ function restoreControllerIdentity(home, identity, environment, spawn) {
|
|
|
491
502
|
// Spawn a detached child through a short-lived Node helper so the synchronous
|
|
492
503
|
// update process can still use the authenticated, bounded readiness handshake
|
|
493
504
|
// shared by Controller startup. No retry, sleep, or new identity inference is
|
|
494
|
-
// hidden here.
|
|
505
|
+
// hidden here. Restoration participates in the same parent-owned lock.
|
|
495
506
|
const helper = [
|
|
496
507
|
"const { spawn } = require('node:child_process');",
|
|
497
508
|
"const values = process.argv.slice(1);",
|
|
509
|
+
"const handoverOwnerPid = Number(values.pop());",
|
|
498
510
|
"const version = values.pop();",
|
|
499
511
|
"const args = JSON.parse(values.pop());",
|
|
500
512
|
"const executable = values.pop();",
|
|
@@ -504,6 +516,7 @@ function restoreControllerIdentity(home, identity, environment, spawn) {
|
|
|
504
516
|
" const { ensureFileTaskControllerIdentity } = await import(runtimeModule);",
|
|
505
517
|
" await ensureFileTaskControllerIdentity(home, { executablePath: executable, args, version }, {",
|
|
506
518
|
" environment: process.env,",
|
|
519
|
+
" handoverOwnerPid,",
|
|
507
520
|
" spawnController: (_home, launchEnv) => {",
|
|
508
521
|
" const child = spawn(executable, args, { detached: true, stdio: 'ignore', env: launchEnv });",
|
|
509
522
|
" child.unref();",
|
|
@@ -518,7 +531,8 @@ function restoreControllerIdentity(home, identity, environment, spawn) {
|
|
|
518
531
|
home,
|
|
519
532
|
identity.executablePath,
|
|
520
533
|
JSON.stringify(identity.args),
|
|
521
|
-
identity.version
|
|
534
|
+
identity.version,
|
|
535
|
+
String(process.pid)
|
|
522
536
|
], { cwd: process.cwd(), env: launchEnvironment, shell: false, stdio: "pipe" });
|
|
523
537
|
assertSpawnOk(result, "restore the previously running Controller identity");
|
|
524
538
|
}
|
|
@@ -585,14 +599,23 @@ function reconcileControllerResourcesForUpdate(home, environment, spawn) {
|
|
|
585
599
|
" process.stdout.write(JSON.stringify({ ok: true, data }));",
|
|
586
600
|
"})().catch((error) => {",
|
|
587
601
|
" const message = error instanceof Error ? error.message : String(error);",
|
|
588
|
-
" process.stderr.write(JSON.stringify({ ok: false, code: 'RUNTIME_ERROR', message }));",
|
|
602
|
+
" process.stderr.write(JSON.stringify({ ok: false, code: 'RUNTIME_ERROR', message, result: error.result }));",
|
|
589
603
|
" process.exitCode = 5;",
|
|
590
604
|
"});"
|
|
591
605
|
].join(" ");
|
|
592
606
|
const result = spawn(process.execPath, ["-e", helper, UPDATE_CONTROLLER_RECONCILIATION_PATH, home], { cwd: process.cwd(), env: { ...environment, YUI_HOME: home }, shell: false });
|
|
593
607
|
if (result.error !== undefined || result.status !== 0) {
|
|
594
608
|
const detail = structuredErrorMessage(result) ?? result.stderr.toString("utf8").trim();
|
|
595
|
-
|
|
609
|
+
const message = `Controller reconciliation failed (exit ${result.status ?? "null"})${detail.length === 0 ? "." : `: ${detail}`}`;
|
|
610
|
+
let evidence;
|
|
611
|
+
try {
|
|
612
|
+
evidence = JSON.parse(result.stderr.toString("utf8")).result;
|
|
613
|
+
}
|
|
614
|
+
catch { /* Unknown child effects are reported below, never retried here. */ }
|
|
615
|
+
if (isReconciliationResult(evidence, home)) {
|
|
616
|
+
throw new UpdateControllerReconciliationError(message, evidence);
|
|
617
|
+
}
|
|
618
|
+
throw new Error(`${message} Cleanup effects are unknown; inspect the exact Home ${home} before retrying.`);
|
|
596
619
|
}
|
|
597
620
|
let parsed;
|
|
598
621
|
try {
|
|
@@ -605,11 +628,19 @@ function reconcileControllerResourcesForUpdate(home, environment, spawn) {
|
|
|
605
628
|
}
|
|
606
629
|
if (!isRecord(parsed)
|
|
607
630
|
|| parsed.ok !== true
|
|
608
|
-
|| !
|
|
609
|
-
|| !Array.isArray(parsed.data.cleaned)
|
|
610
|
-
|| parsed.data.cleaned.some((id) => typeof id !== "string")) {
|
|
631
|
+
|| !isReconciliationResult(parsed.data, home)) {
|
|
611
632
|
throw new Error("Controller reconciliation returned an invalid structured result.");
|
|
612
633
|
}
|
|
634
|
+
return parsed.data;
|
|
635
|
+
}
|
|
636
|
+
function isReconciliationResult(value, home) {
|
|
637
|
+
return isRecord(value) && value.home === resolve(home)
|
|
638
|
+
&& Array.isArray(value.cleaned) && value.cleaned.every(id => typeof id === "string")
|
|
639
|
+
&& Array.isArray(value.attempts) && value.attempts.every(attempt => (isRecord(attempt) && isRecord(attempt.resource) && typeof attempt.resource.id === "string"
|
|
640
|
+
&& ["cleaned", "absent", "unknown"].includes(String(attempt.outcome))
|
|
641
|
+
&& (attempt.error === undefined || typeof attempt.error === "string")))
|
|
642
|
+
&& Array.isArray(value.remaining) && value.remaining.every(resource => (isRecord(resource) && typeof resource.id === "string"))
|
|
643
|
+
&& ["observedAt", "observationError", "lockReleaseError"].every(key => (value[key] === undefined || typeof value[key] === "string"));
|
|
613
644
|
}
|
|
614
645
|
function structuredErrorMessage(result) {
|
|
615
646
|
for (const buffer of [result.stderr, result.stdout]) {
|
|
@@ -779,7 +810,7 @@ function interpretPreflight(result) {
|
|
|
779
810
|
}
|
|
780
811
|
const outcome = typeof data.outcome === "string" ? data.outcome : undefined;
|
|
781
812
|
// EXIT/OUTCOME CONSISTENCY (P1-2): the one success-class internal preflight
|
|
782
|
-
// outcome must exit 0. A user dry-run,
|
|
813
|
+
// outcome must exit 0. A user dry-run, a direct classification outcome,
|
|
783
814
|
// or any other spelling is not this contract and is never promoted to green.
|
|
784
815
|
if (outcome === "update-preflight" && result.status !== 0) {
|
|
785
816
|
return {
|
package/dist/cli.js
CHANGED
|
@@ -45,7 +45,7 @@ import { assertTaskDeliveryAuthority, taskLocalActor } from "./task/taskAuthorit
|
|
|
45
45
|
import { runTaskBaseStatusCommand } from "./commands/taskBaseCommands.js";
|
|
46
46
|
import { runTaskChangeSetCommand } from "./commands/taskChangeSetCommands.js";
|
|
47
47
|
import { assertWorkItemDependenciesCompletedForCommand, dispatchPreparedReviewRound, failPendingReviewRound, parseTaskArchiveArguments, parseTaskCompletionRequest, planReplicatedWorkItemLanes, preflightTaskCompletion, preserveReviewRoundWorkspace, previewTaskRoleAgentConfigurationMutation, requireWorkItemAssignee, runTaskCommand, validateTaskArchiveRequest } from "./commands/taskCommands.js";
|
|
48
|
-
import {
|
|
48
|
+
import { verifyTaskCompletionPublishedTree } from "./commands/taskCompletionGate.js";
|
|
49
49
|
import { finalizeStoppedTaskExecution, parseTaskExecutionStartRequest, parseTaskExecutionStopRequest, startTaskExecutionCommand, stopTaskExecutionCommand } from "./commands/taskExecutionCommands.js";
|
|
50
50
|
import { runTaskIntegrationCommand } from "./commands/taskIntegrationCommands.js";
|
|
51
51
|
import { runTaskOverlapCommand } from "./commands/taskOverlapCommands.js";
|
|
@@ -225,9 +225,7 @@ export async function main() {
|
|
|
225
225
|
if (args[0] === "update") {
|
|
226
226
|
if (jsonOutput)
|
|
227
227
|
throw usageError("Update does not support --json.");
|
|
228
|
-
|
|
229
|
-
throw usageError("Update usage: yui update");
|
|
230
|
-
process.exitCode = runUpdateCommand();
|
|
228
|
+
process.exitCode = runUpdateCommand(args.slice(1));
|
|
231
229
|
return;
|
|
232
230
|
}
|
|
233
231
|
if (args[0] === "release") {
|
|
@@ -1384,6 +1382,8 @@ export async function main() {
|
|
|
1384
1382
|
}
|
|
1385
1383
|
let completionSummary;
|
|
1386
1384
|
let completionPublishedTreeProof;
|
|
1385
|
+
let completionBaseFreshness;
|
|
1386
|
+
let completionWarnings = [];
|
|
1387
1387
|
if (resolved[1] === "base" && resolved[2] === "status") {
|
|
1388
1388
|
const result = await runTaskBaseStatusCommand(resolved.slice(3), store);
|
|
1389
1389
|
emit(result.output, false, result.data);
|
|
@@ -1400,8 +1400,8 @@ export async function main() {
|
|
|
1400
1400
|
if (resolved[1] === "complete" && resolved[2] !== undefined) {
|
|
1401
1401
|
const completionRequest = parseTaskCompletionRequest(resolved.slice(2));
|
|
1402
1402
|
completionSummary = completionRequest.summary;
|
|
1403
|
-
const refreshRemote =
|
|
1404
|
-
const completion = preflightTaskCompletion(
|
|
1403
|
+
const refreshRemote = completionRequest.refreshRemote;
|
|
1404
|
+
const completion = preflightTaskCompletion(completionRequest.taskId, store, {
|
|
1405
1405
|
environment: process.env,
|
|
1406
1406
|
...(taskFinalReviewContract === undefined
|
|
1407
1407
|
? {}
|
|
@@ -1412,34 +1412,25 @@ export async function main() {
|
|
|
1412
1412
|
// Publication proof resolves its exact commit. Without the flag the
|
|
1413
1413
|
// command remains offline and preserves the existing proof-first path.
|
|
1414
1414
|
const refreshedFreshness = refreshRemote
|
|
1415
|
-
? await inspectTaskBaseFreshness(
|
|
1415
|
+
? await inspectTaskBaseFreshness(completionRequest.taskId, store, { refresh: true })
|
|
1416
1416
|
: undefined;
|
|
1417
1417
|
if (completionRequest.acceptedPublishedTreePublicationId !== undefined) {
|
|
1418
1418
|
completionPublishedTreeProof = await verifyTaskCompletionPublishedTree(completionRequest.taskId, completionRequest.acceptedPublishedTreePublicationId, store);
|
|
1419
1419
|
}
|
|
1420
|
-
|
|
1421
|
-
?? await inspectTaskBaseFreshness(
|
|
1422
|
-
|
|
1420
|
+
completionBaseFreshness = refreshedFreshness
|
|
1421
|
+
?? await inspectTaskBaseFreshness(completionRequest.taskId, store);
|
|
1422
|
+
completionWarnings = assertTaskBaseFreshnessForCompletion(completionBaseFreshness, {
|
|
1423
1423
|
...(completionPublishedTreeProof === undefined
|
|
1424
1424
|
? {}
|
|
1425
1425
|
: {
|
|
1426
1426
|
acceptedPublishedTreeProjectId: completionPublishedTreeProof.projectId
|
|
1427
1427
|
})
|
|
1428
|
-
})
|
|
1428
|
+
});
|
|
1429
|
+
for (const warning of completionWarnings) {
|
|
1429
1430
|
process.stderr.write(`Warning: ${warning}\n`);
|
|
1430
1431
|
}
|
|
1431
|
-
//
|
|
1432
|
-
//
|
|
1433
|
-
if (refreshRemote) {
|
|
1434
|
-
const reconciled = await reconcileTaskRemoteBaselines(resolved[2], store, home, { environment: process.env, jobPort: createControllerIntegrationJobPort(home, { environment: process.env }) });
|
|
1435
|
-
if (reconciled.length > 0) {
|
|
1436
|
-
const updates = reconciled.map((entry) => (`${entry.projectId}: ${entry.fromCommit} -> ${entry.toCommit} `
|
|
1437
|
-
+ `(Integration ${entry.integrationId})`)).join("; ");
|
|
1438
|
-
throw usageError(`Remote baseline reconciliation advanced Task ${resolved[2]} (${updates}). `
|
|
1439
|
-
+ "The Task remains active so the Leader can inspect the new authoritative head, "
|
|
1440
|
-
+ "decide how prior Review evidence applies, and retry task complete.");
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1432
|
+
// Refresh observes remote objects only. Choosing and executing an
|
|
1433
|
+
// upstream Integration belongs to its explicit command, never completion.
|
|
1443
1434
|
}
|
|
1444
1435
|
}
|
|
1445
1436
|
let releaseReviewHandoverLock;
|
|
@@ -1628,9 +1619,24 @@ export async function main() {
|
|
|
1628
1619
|
emitControlFailure(result.output, failureCode, result.data);
|
|
1629
1620
|
return;
|
|
1630
1621
|
}
|
|
1631
|
-
|
|
1622
|
+
const commandData = reviewData === undefined
|
|
1632
1623
|
? result.data
|
|
1633
|
-
: { command: result.data, ...reviewData }
|
|
1624
|
+
: { command: result.data, ...reviewData };
|
|
1625
|
+
const completionData = resolved[1] === "complete" ? {
|
|
1626
|
+
// Review dispatch may have advanced or failed after the command
|
|
1627
|
+
// prepared its Round. Report that observed stage, not preparation.
|
|
1628
|
+
...(reviewData === undefined ? {} : {
|
|
1629
|
+
stage: reviewRoundFromCommandData(reviewData)?.status === "running"
|
|
1630
|
+
? "review-running"
|
|
1631
|
+
: "review-blocked",
|
|
1632
|
+
projectHeads: result.data.projectHeads
|
|
1633
|
+
}),
|
|
1634
|
+
baseFreshness: completionBaseFreshness ?? null,
|
|
1635
|
+
warnings: completionWarnings
|
|
1636
|
+
} : undefined;
|
|
1637
|
+
emit(`${result.output}${reviewOutput}`, false, completionData === undefined
|
|
1638
|
+
? commandData
|
|
1639
|
+
: { ...commandData, ...completionData });
|
|
1634
1640
|
return;
|
|
1635
1641
|
}
|
|
1636
1642
|
if (jsonOutput && result.kind !== "session-stop"
|
|
@@ -2431,7 +2437,10 @@ async function executeOperatorSessionControl(control, home, store, runtime, tmux
|
|
|
2431
2437
|
await ensureFileTaskController(home, { environment: process.env });
|
|
2432
2438
|
if (paneRunning
|
|
2433
2439
|
|| (active !== undefined
|
|
2434
|
-
&& active.status === "active")
|
|
2440
|
+
&& active.status === "active")
|
|
2441
|
+
|| (sessionSet?.providerBinding?.run != null
|
|
2442
|
+
&& ["submitting", "accepted", "delivery-unknown"].includes(sessionSet.providerBinding.run.status))
|
|
2443
|
+
|| (sessionSet?.providerBinding?.goal != null && sessionSet.providerBinding.goal.status !== "complete")) {
|
|
2435
2444
|
await runtime.stopGlobalRoleSession(role.name);
|
|
2436
2445
|
}
|
|
2437
2446
|
applyOperatorSessionControl(control, store);
|
|
@@ -202,7 +202,7 @@ export function renderRuntimeIdentitySection(input) {
|
|
|
202
202
|
` Source commit ${build.sourceCommit}`,
|
|
203
203
|
` Node ${build.nodeVersion} (${build.platform})`,
|
|
204
204
|
` Storage version ${storage.storageVersion} (status ${storage.storageStatus}, minimum ${storage.minimumStorageVersion}) · backend ${storage.configuredBackend} · worker ${storage.workerEnabled ? "on" : "off"}`,
|
|
205
|
-
` Store files
|
|
205
|
+
` Store files yui.db ${storage.physicalDatabase.present ? "present" : "absent"}${storage.physicalDatabase.wal ? " +WAL" : ""}${storage.physicalDatabase.present && storage.physicalDatabase.health !== UNSUPPORTED ? ` (${storage.physicalDatabase.health})` : ""}`
|
|
206
206
|
];
|
|
207
207
|
for (const finding of storage.findings) {
|
|
208
208
|
lines.push(` ! ${finding.severity === "contradiction" ? "CONTRADICTION" : finding.severity === "needs-repair" ? "NEEDS-REPAIR" : "warning"} ${finding.code}: ${finding.message}`, ` remediation: ${finding.remediation}`);
|
|
@@ -258,8 +258,8 @@ export function renderExecutionAudit(report, width = defaultTableWidth()) {
|
|
|
258
258
|
}
|
|
259
259
|
if (report.storage.status === "ok" && report.storage.data !== undefined) {
|
|
260
260
|
const storage = report.storage.data;
|
|
261
|
-
lines.push("", `Storage: backend ${storage.backend} ·
|
|
262
|
-
+ ` · runtime/ ${formatBytes(storage.runtimeDirBytes)}
|
|
261
|
+
lines.push("", `Storage: backend ${storage.backend} · yui.db ${formatBytes(storage.databaseBytes)}`
|
|
262
|
+
+ ` · runtime/ ${formatBytes(storage.runtimeDirBytes)}`);
|
|
263
263
|
}
|
|
264
264
|
else {
|
|
265
265
|
lines.push("", ...sectionError("storage", report));
|
|
@@ -238,7 +238,7 @@ async function cloneProject(args, store, options) {
|
|
|
238
238
|
// A remote-URL binding is Home-managed by default: its canonical repository
|
|
239
239
|
// lives below the persistent Home, so the runtime never depends on a
|
|
240
240
|
// user-controlled checkout path. --external is an explicit opt-in to the
|
|
241
|
-
//
|
|
241
|
+
// user-owned clone-inside-the-workspace mode.
|
|
242
242
|
let destination;
|
|
243
243
|
let ownership;
|
|
244
244
|
if (parsed.external) {
|
|
@@ -341,55 +341,26 @@ export function createReleaseActivatePorts(overrides = {}) {
|
|
|
341
341
|
}),
|
|
342
342
|
runPreflight: overrides.runPreflight ?? ((releaseDir, home) => {
|
|
343
343
|
const cli = join(releaseDir, "dist", "cli.js");
|
|
344
|
-
|
|
344
|
+
// The target owns current-record and database integrity validation.
|
|
345
|
+
// Activation never migrates; a valid but older Home still blocks here.
|
|
346
|
+
const result = spawnSync(process.execPath, [cli, "--json", "upgrade", "--update-preflight"], {
|
|
345
347
|
env: { ...process.env, YUI_HOME: home, NO_COLOR: "1" },
|
|
346
348
|
encoding: "utf8",
|
|
347
349
|
timeout: 60_000
|
|
348
350
|
});
|
|
349
351
|
let report;
|
|
350
352
|
try {
|
|
351
|
-
|
|
352
|
-
report = envelope.data;
|
|
353
|
+
report = JSON.parse(result.stdout);
|
|
353
354
|
}
|
|
354
355
|
catch (error) {
|
|
355
356
|
throw new Error(`Release preflight produced no JSON (exit ${result.status}): `
|
|
356
357
|
+ `${messageOf(error)} ${result.stderr.trim()}`);
|
|
357
358
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
throw new Error("Release preflight report has no storage checks.");
|
|
364
|
-
}
|
|
365
|
-
const required = ["storage compatibility"];
|
|
366
|
-
const failed = required.filter((name) => {
|
|
367
|
-
const check = checks.find((candidate) => candidate !== null
|
|
368
|
-
&& typeof candidate === "object"
|
|
369
|
-
&& candidate.name === name);
|
|
370
|
-
return check === undefined
|
|
371
|
-
|| check.status !== "ok";
|
|
372
|
-
});
|
|
373
|
-
if (failed.length > 0) {
|
|
374
|
-
throw new Error(`Release preflight storage compatibility checks failed: ${failed.join(", ")}.`);
|
|
375
|
-
}
|
|
376
|
-
// Doctor proves the target's storage, not the pinned code in existing
|
|
377
|
-
// Hosts. Ask the target's upgrade boundary for its independent Host
|
|
378
|
-
// protocol proof as well; release activation itself still never migrates.
|
|
379
|
-
const hostCheck = spawnSync(process.execPath, [cli, "--json", "upgrade", "--update-preflight"], {
|
|
380
|
-
env: { ...process.env, YUI_HOME: home, NO_COLOR: "1" },
|
|
381
|
-
encoding: "utf8", timeout: 60_000
|
|
382
|
-
});
|
|
383
|
-
let preflight;
|
|
384
|
-
try {
|
|
385
|
-
preflight = JSON.parse(hostCheck.stdout).data;
|
|
386
|
-
}
|
|
387
|
-
catch {
|
|
388
|
-
throw new Error(`Release Host preflight produced no JSON: ${hostCheck.stderr.trim()}`);
|
|
389
|
-
}
|
|
390
|
-
if (hostCheck.status !== 0 || preflight?.outcome !== "update-preflight"
|
|
391
|
-
|| preflight.status !== "already-current") {
|
|
392
|
-
throw new Error(`Release Host compatibility preflight failed: ${preflight?.message ?? preflight?.status ?? "unknown"}. ${preflight?.action ?? ""}`);
|
|
359
|
+
if (result.status !== 0 || report?.ok !== true
|
|
360
|
+
|| report.data?.outcome !== "update-preflight"
|
|
361
|
+
|| report.data.status !== "already-current") {
|
|
362
|
+
throw new Error(`Release storage preflight failed: ${report?.data?.message ?? report?.data?.status ?? "invalid response"}. `
|
|
363
|
+
+ `${report?.data?.action ?? result.stderr.trim()}`);
|
|
393
364
|
}
|
|
394
365
|
}),
|
|
395
366
|
killOwnedProcess: overrides.killOwnedProcess ?? ((owner) => {
|