@zq-silk/yui 0.8.7 → 0.8.9

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/dist/cli.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
2
3
  import { readFileSync } from "node:fs";
3
4
  import { join, resolve } from "node:path";
4
5
  import { createInterface } from "node:readline/promises";
@@ -48,7 +49,7 @@ import { runWorkflowCommandAsync } from "./commands/workflowCommands.js";
48
49
  import { createUpdatePorts } from "./cli/updatePorts.js";
49
50
  import { createReleaseWorkflowPorts } from "./release/releaseWorkflowPorts.js";
50
51
  import { acquireHandoverLock, readRuntimeIdentity } from "./release/runtimeRelease.js";
51
- import { renderReleaseActivateResult, renderReleaseInstallResult, renderReleaseList, runReleaseActivate, runReleaseInstall, runReleaseList } from "./commands/releaseCommands.js";
52
+ import { renderReleaseActivateResult, renderReleaseInstallResult, renderReleaseList, resolveReleaseActivationDriver, runReleaseActivate, runReleaseInstall, runReleaseList } from "./commands/releaseCommands.js";
52
53
  import { reconcileTaskRemoteBaselines, verifyTaskCompletionPublishedTree } from "./commands/taskCompletionGate.js";
53
54
  import { runTaskBaseStatusCommand } from "./commands/taskBaseCommands.js";
54
55
  import { assertTaskBaseFreshnessForCompletion, inspectTaskBaseFreshness } from "./repository/taskBaseFreshness.js";
@@ -112,6 +113,22 @@ void main().catch((error) => {
112
113
  process.exitCode = 5;
113
114
  });
114
115
  export async function main() {
116
+ const delegatedDriver = explicitReleaseActivationDriver();
117
+ if (delegatedDriver !== null) {
118
+ const delegated = spawnSync(process.execPath, [delegatedDriver, ...process.argv.slice(2)], {
119
+ cwd: process.cwd(),
120
+ env: process.env,
121
+ stdio: "inherit"
122
+ });
123
+ if (delegated.error !== undefined) {
124
+ throw runtimeError(`Target release activation driver could not start: ${delegated.error.message}`);
125
+ }
126
+ if (delegated.signal !== null) {
127
+ throw runtimeError(`Target release activation driver stopped by signal ${delegated.signal}.`);
128
+ }
129
+ process.exitCode = delegated.status ?? 5;
130
+ return;
131
+ }
115
132
  const { contract: taskFinalReviewContract, verifiedStore, controlPlane: exactCurrentControlPlane } = await preflightManagedTaskControlPlane();
116
133
  if (args.length === 0) {
117
134
  emit(renderCommandHelp((await import("./cli/commandCatalog.js")).ROOT_COMMAND, VERSION));
@@ -1360,6 +1377,15 @@ export async function main() {
1360
1377
  }
1361
1378
  throw usageError(`Command is not connected to the restored FileTaskStore framework yet: ${resolved[0]}.`, renderCommandHelp(invocation.node, VERSION));
1362
1379
  }
1380
+ function explicitReleaseActivationDriver() {
1381
+ if (args.length !== 3
1382
+ || args[0] !== "release"
1383
+ || args[1] !== "activate"
1384
+ || args[2].startsWith("-")) {
1385
+ return null;
1386
+ }
1387
+ return resolveReleaseActivationDriver(resolveYuiHome(process.env), args[2], fileURLToPath(import.meta.url));
1388
+ }
1363
1389
  async function preflightManagedTaskControlPlane() {
1364
1390
  if (exactControlInvocation.error !== undefined) {
1365
1391
  throw new Error(exactControlInvocation.error);
@@ -8,7 +8,7 @@ import { join, resolve } from "node:path";
8
8
  import { callController, ControllerClientError } from "../core/controllerClient.js";
9
9
  import { readLinuxProcessStartIdentity } from "../controller/domainIdentity.js";
10
10
  import { activateRelease } from "../release/releaseHandover.js";
11
- import { assertReleaseIsNotWorktreeOrLinked, readActiveReleasePointer, readReleaseManifest, releasesDirectory, releaseDirectoryFor, verifyReleaseIntegrity, writeSmokeReceipt } from "../release/runtimeRelease.js";
11
+ import { assertReleaseIsNotWorktreeOrLinked, detectRunningRelease, readActiveReleasePointer, readReleaseManifest, readSmokeReceipt, releasesDirectory, releaseDirectoryFor, verifyReleaseIntegrity, writeSmokeReceipt } from "../release/runtimeRelease.js";
12
12
  /**
13
13
  * Installs a runtime package directory into the Home's immutable release
14
14
  * tree. The source must carry a release manifest, must not be a Git worktree
@@ -123,7 +123,7 @@ export function runReleaseList(home) {
123
123
  version: manifest.version,
124
124
  buildId: manifest.buildId,
125
125
  packageDigest: manifest.packageDigest,
126
- smoke: existsSync(join(releaseDir, "smoke-receipt.json"))
126
+ smoke: hasMatchingSmokeReceipt(releaseDir, manifest)
127
127
  };
128
128
  }
129
129
  catch {
@@ -142,6 +142,8 @@ export function runReleaseList(home) {
142
142
  /** Resolves an installed release by directory name or build ID. */
143
143
  export function resolveInstalledRelease(home, releaseId) {
144
144
  const directory = releasesDirectory(home);
145
+ if (!existsSync(directory))
146
+ return null;
145
147
  for (const entry of readdirSync(directory, { withFileTypes: true })) {
146
148
  if (!entry.isDirectory())
147
149
  continue;
@@ -156,18 +158,45 @@ export function resolveInstalledRelease(home, releaseId) {
156
158
  + "Reinstall it with `yui release install <source-dir>` before activating.", { cause: error });
157
159
  }
158
160
  }
161
+ const releaseDir = join(directory, entry.name);
162
+ let manifest;
159
163
  try {
160
- const manifest = readReleaseManifest(join(directory, entry.name));
161
- if (manifest.buildId === releaseId) {
162
- return { releaseDir: join(directory, entry.name), manifest };
163
- }
164
+ manifest = readReleaseManifest(releaseDir);
164
165
  }
165
166
  catch {
166
167
  continue;
167
168
  }
169
+ if (manifest.buildId !== releaseId)
170
+ continue;
171
+ try {
172
+ return { releaseDir, manifest: verifyReleaseIntegrity(releaseDir) };
173
+ }
174
+ catch (error) {
175
+ throw new Error(`Release ${releaseId} failed integrity verification and may have drifted: `
176
+ + `${error instanceof Error ? error.message : String(error)}. `
177
+ + "Reinstall it with `yui release install <source-dir>` before activating.", { cause: error });
178
+ }
168
179
  }
169
180
  return null;
170
181
  }
182
+ /**
183
+ * Selects the verified target release CLI as the activation driver. Ordinary
184
+ * commands remain on the globally installed CLI; only an explicit target
185
+ * activation crosses into the target package so its handover deadlines and
186
+ * protocol own the complete operation.
187
+ */
188
+ export function resolveReleaseActivationDriver(home, releaseId, currentCliPath) {
189
+ const resolved = resolveInstalledRelease(home, releaseId);
190
+ if (resolved === null)
191
+ return null;
192
+ if (!hasMatchingSmokeReceipt(resolved.releaseDir, resolved.manifest))
193
+ return null;
194
+ const running = detectRunningRelease(currentCliPath);
195
+ if (running !== null && resolve(running.releaseDir) === resolve(resolved.releaseDir)) {
196
+ return null;
197
+ }
198
+ return join(resolved.releaseDir, "dist", "cli.js");
199
+ }
171
200
  /** Runs the atomic Controller handover for one installed release. */
172
201
  export async function runReleaseActivate(home, releaseId, options = {}) {
173
202
  let resolved;
@@ -192,11 +221,11 @@ export async function runReleaseActivate(home, releaseId, options = {}) {
192
221
  recoverable: true
193
222
  };
194
223
  }
195
- if (!existsSync(join(resolved.releaseDir, "smoke-receipt.json"))) {
224
+ if (!hasMatchingSmokeReceipt(resolved.releaseDir, resolved.manifest)) {
196
225
  return {
197
226
  outcome: "aborted",
198
227
  phase: "resolve",
199
- message: `Release has no smoke receipt: ${releaseId}.`,
228
+ message: `Release has no matching smoke receipt: ${releaseId}.`,
200
229
  action: "Reinstall the release; the smoke receipt is written at install time.",
201
230
  recoverable: true
202
231
  };
@@ -217,6 +246,13 @@ export async function runReleaseActivate(home, releaseId, options = {}) {
217
246
  : { dualOwnerGraceMs: options.dualOwnerGraceMs })
218
247
  });
219
248
  }
249
+ function hasMatchingSmokeReceipt(releaseDir, manifest) {
250
+ const receipt = readSmokeReceipt(releaseDir);
251
+ return receipt !== null
252
+ && receipt.version === manifest.version
253
+ && receipt.buildId === manifest.buildId
254
+ && receipt.packageDigest === manifest.packageDigest;
255
+ }
220
256
  /** Renders the install result as concise CLI text. */
221
257
  export function renderReleaseInstallResult(result) {
222
258
  switch (result.outcome) {
@@ -79,10 +79,21 @@ export function createProductionRuntimeIdentityPorts(packageRoot, entryPath, env
79
79
  }
80
80
  function readGitHead(packageRoot) {
81
81
  try {
82
- const result = spawnSync("git", ["-C", packageRoot, "rev-parse", "HEAD"], { encoding: "utf8", timeout: 1_500, stdio: ["ignore", "pipe", "ignore"] });
83
- if (result.status !== 0)
82
+ const resolvedPackageRoot = realpathSync(packageRoot);
83
+ const rootResult = spawnSync("git", ["-C", resolvedPackageRoot, "rev-parse", "--show-toplevel"], { encoding: "utf8", timeout: 1_500, stdio: ["ignore", "pipe", "ignore"] });
84
+ if (rootResult.status !== 0)
84
85
  return null;
85
- const head = result.stdout.trim();
86
+ const repositoryRoot = realpathSync(rootResult.stdout.trim());
87
+ // `git -C` searches parent directories. A globally installed package may
88
+ // therefore appear to belong to an unrelated repository that happens to
89
+ // contain the package manager directory (for example ~/.nvm). Only the
90
+ // package's own checkout is valid source identity evidence.
91
+ if (repositoryRoot !== resolvedPackageRoot)
92
+ return null;
93
+ const headResult = spawnSync("git", ["-C", resolvedPackageRoot, "rev-parse", "HEAD"], { encoding: "utf8", timeout: 1_500, stdio: ["ignore", "pipe", "ignore"] });
94
+ if (headResult.status !== 0)
95
+ return null;
96
+ const head = headResult.stdout.trim();
86
97
  return /^[a-f0-9]{7,40}$/u.test(head) ? head : null;
87
98
  }
88
99
  catch {
@@ -135,8 +135,8 @@ async function activateLocked(ports, locked) {
135
135
  };
136
136
  }
137
137
  // 6) Switch the active release pointer. This is the atomic commit point:
138
- // after it succeeds, the stable launcher and new Sessions resolve the new
139
- // release.
138
+ // after it succeeds, Controller lifecycle operations resolve the new
139
+ // release. The ordinary CLI and managed Session wrappers stay on `yui`.
140
140
  const pointer = Object.freeze({
141
141
  schemaVersion: 1,
142
142
  releaseId,
@@ -4,9 +4,10 @@
4
4
  *
5
5
  * A release is a self-contained, content-addressed runtime package unpacked
6
6
  * below `runtime/releases/<version>-<package-sha256>/`. The Home's active
7
- * release pointer (`runtime/active-release.json`) names exactly one release;
8
- * the stable launcher shim resolves the CLI through that pointer instead of
9
- * symlinking a development checkout.
7
+ * release pointer (`runtime/active-release.json`) names exactly one Controller
8
+ * release. The ordinary CLI remains the globally installed package; only an
9
+ * explicit target activation delegates to the verified target release CLI so
10
+ * that release owns its handover protocol and deadlines.
10
11
  *
11
12
  * The Controller writes its provenance to `runtime/runtime-identity.json` on
12
13
  * every start, and a versioned handover fence (`runtime/handover-fence.json`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zq-silk/yui",
3
- "version": "0.8.7",
3
+ "version": "0.8.9",
4
4
  "description": "Local control plane for long-running native agent CLI sessions backed by tmux.",
5
5
  "license": "MIT",
6
6
  "private": false,