@timurproko/a1 0.1.8-dev.508 → 0.1.8-dev.512

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.
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Protocol: the updater that installed this tree starts this entry with --data-dir and
4
+ // --target-version and reads one JSON progress event per line. The tree activates itself
5
+ // with its own release code, so the updater needs to know nothing else about its layout.
6
+ const { runActivationEntry } = await import("../dist/foundation/release/update-activation.js");
7
+
8
+ process.exitCode = await runActivationEntry(process.argv.slice(2), import.meta.url);
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Compatibility: updaters older than 0.1.8-dev.479 run this entry from the tree they just
4
+ // installed to point a terminal-package proxy that no longer exists at that tree. Those
5
+ // updaters treat a missing file as a failure worth a stack trace on the user's terminal,
6
+ // so the entry stays, does nothing, and exits successfully. Newer updaters never run it.
7
+ process.exitCode = 0;
@@ -22,6 +22,8 @@ export { STABLE_RELEASE_SCHEMA, createStableReleaseEvidence, verifyStableRegistr
22
22
  export type { StableRegistryState, StableRegistryVerificationOptions, StableReleaseEvidence, StableReleaseEvidenceInput } from "./stable-release.js";
23
23
  export { PRODUCT_PACKAGE, assertUpdatePerformanceBudget, createNpmProcessRunner, createUpdateLifecycleCoordinator, planUpdateOwnership, renderUpdateProgressBar, runSelfUpdate, } from "./update.js";
24
24
  export type { ProcessRequest, ProcessResult, SelfUpdateOptions, UpdateActivationPhase, UpdateChannel, UpdateFileSystem, UpdateLifecycleCoordinator, UpdateMaterializationProgress, UpdateMeasuredPhase, UpdateOutput, UpdateOwnershipAction, UpdatePackageReplacementInput, UpdatePerformanceEvidence, UpdatePhaseTimingEvent, UpdateProcessRunner, UpdateTransactionJournal, } from "./update.js";
25
+ export { UPDATE_ACTIVATION_CONTRACT, UPDATE_ACTIVATION_ENTRY, UPDATE_ACTIVATION_MANIFEST_FIELD, activateInstalledRelease, delegateActivation, readActivationContracts, runActivationEntry, } from "./update-activation.js";
26
+ export type { UpdateActivationCallbacks, UpdateActivationEvent, UpdateActivationRequest } from "./update-activation.js";
25
27
  export { selectSupervisorLaunchReleaseId, selectUpdateLaunchRelease } from "./update-launch.js";
26
28
  export { UPDATE_RECOVERY_SCHEMA, cleanupUpdateRecoveryCapsules, inspectUpdateLauncherSet, prepareUpdateRecoveryCapsule, readUpdateRecoveryCapsule, removeUpdateRecoveryCapsule, runProtectedPackageReplacement, updateLauncherPaths, } from "./update-recovery.js";
27
29
  export type { ProtectedPackageReplacementOptions, ProtectedPackageReplacementResult, UpdateRecoveryCapsule, UpdateRecoveryResult } from "./update-recovery.js";
@@ -10,6 +10,7 @@ export { RELEASE_MANIFEST_FILENAME, assertImmutableExecutionRoot, consumeMateria
10
10
  export { createRestartSeal, readRestartCertifiedRelease, releaseCertificationDocument, restartSealDigest } from "./restart-certification.js";
11
11
  export { STABLE_RELEASE_SCHEMA, createStableReleaseEvidence, verifyStableRegistry } from "./stable-release.js";
12
12
  export { PRODUCT_PACKAGE, assertUpdatePerformanceBudget, createNpmProcessRunner, createUpdateLifecycleCoordinator, planUpdateOwnership, renderUpdateProgressBar, runSelfUpdate, } from "./update.js";
13
+ export { UPDATE_ACTIVATION_CONTRACT, UPDATE_ACTIVATION_ENTRY, UPDATE_ACTIVATION_MANIFEST_FIELD, activateInstalledRelease, delegateActivation, readActivationContracts, runActivationEntry, } from "./update-activation.js";
13
14
  export { selectSupervisorLaunchReleaseId, selectUpdateLaunchRelease } from "./update-launch.js";
14
15
  export { UPDATE_RECOVERY_SCHEMA, cleanupUpdateRecoveryCapsules, inspectUpdateLauncherSet, prepareUpdateRecoveryCapsule, readUpdateRecoveryCapsule, removeUpdateRecoveryCapsule, runProtectedPackageReplacement, updateLauncherPaths, } from "./update-recovery.js";
15
16
  export { UPDATE_JOURNAL_SCHEMA, UpdateTransactionStore } from "./update-transaction.js";
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The one activation contract an installed tree can offer the updater that installed it.
3
+ * The updater knows nothing else about the tree: the manifest names the contracts the tree
4
+ * serves, and this entry drives its activation with the tree's own release code.
5
+ */
6
+ export declare const UPDATE_ACTIVATION_CONTRACT = "activate-v1";
7
+ export declare const UPDATE_ACTIVATION_ENTRY = "bin/activate.js";
8
+ /** Manifest field listing the activation contracts an installed tree serves. */
9
+ export declare const UPDATE_ACTIVATION_MANIFEST_FIELD = "updateActivationContracts";
10
+ export type UpdateActivationPhase = "materialized" | "certified" | "active-reference-committed";
11
+ /**
12
+ * Copying the release is the longest step with nothing to say for itself, so it
13
+ * reports the files it has written against the files it must write. A caller that
14
+ * shows progress can then move with the work instead of guessing at it.
15
+ */
16
+ export interface UpdateMaterializationProgress {
17
+ readonly completed: number;
18
+ readonly total: number;
19
+ }
20
+ export interface UpdateActivationCallbacks {
21
+ readonly phase: (phase: UpdateActivationPhase) => Promise<void>;
22
+ readonly onMaterializing?: (progress: UpdateMaterializationProgress) => void;
23
+ readonly onWarmup?: (state: "started" | "completed") => void;
24
+ }
25
+ export interface UpdateActivationRequest {
26
+ readonly packageRoot: string;
27
+ readonly dataDir: string;
28
+ readonly targetVersion: string;
29
+ readonly environment: NodeJS.ProcessEnv;
30
+ }
31
+ /** One line of the activator's progress stream; `failed` carries the reason and ends it. */
32
+ export type UpdateActivationEvent = {
33
+ readonly event: "materializing";
34
+ readonly completed: number;
35
+ readonly total: number;
36
+ } | {
37
+ readonly event: "phase";
38
+ readonly phase: UpdateActivationPhase;
39
+ } | {
40
+ readonly event: "warmup";
41
+ readonly state: "started" | "completed";
42
+ } | {
43
+ readonly event: "completed";
44
+ } | {
45
+ readonly event: "failed";
46
+ readonly message: string;
47
+ };
48
+ /**
49
+ * Activate the installed tree at `packageRoot` with this process's own release code:
50
+ * materialize it into the immutable store, certify, warm, verify supervision, and only
51
+ * then move the active reference. This is what the installed tree runs for itself through
52
+ * its activation entry, and what an updater runs in-process for a tree older than the contract.
53
+ */
54
+ export declare function activateInstalledRelease(request: UpdateActivationRequest, callbacks: UpdateActivationCallbacks): Promise<void>;
55
+ /**
56
+ * Which activation contracts the installed tree serves, read from its manifest. A tree
57
+ * without the field, or without a readable manifest, serves none and is activated in-process.
58
+ */
59
+ export declare function readActivationContracts(packageRoot: string, read?: (path: string) => Promise<string>): Promise<readonly string[]>;
60
+ /**
61
+ * Drive the installed tree's own activator and relay its progress. The updater passes
62
+ * the tree what the tree cannot know (the data directory, the target it must match) and
63
+ * learns from it only the phases it reached; every path inside the tree is the tree's business.
64
+ */
65
+ export declare function delegateActivation(request: UpdateActivationRequest, callbacks: UpdateActivationCallbacks): Promise<void>;
66
+ /**
67
+ * Body of the installed tree's activation entry: activate the tree this code ships in,
68
+ * writing one progress event per line so the updater that started it can follow along.
69
+ */
70
+ export declare function runActivationEntry(argv: readonly string[], entryUrl: string, io?: {
71
+ write(line: string): void;
72
+ }, environment?: NodeJS.ProcessEnv): Promise<number>;
@@ -0,0 +1,204 @@
1
+ import { spawn } from "node:child_process";
2
+ import { readFile } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { PRODUCT_TEXT } from "../../product-identity.js";
6
+ import { certifyMaterializedRelease, ensureSupervisor } from "./bootstrap.js";
7
+ import { CohortStateStore } from "./cohort-state.js";
8
+ import { encodeFrame, LineFrameDecoder } from "../protocol/index.js";
9
+ import { resolveProductPaths } from "../lifecycle/index.js";
10
+ import { materializeRelease } from "./release-store.js";
11
+ import { warmMaterializedRelease } from "./warmup.js";
12
+ /**
13
+ * The one activation contract an installed tree can offer the updater that installed it.
14
+ * The updater knows nothing else about the tree: the manifest names the contracts the tree
15
+ * serves, and this entry drives its activation with the tree's own release code.
16
+ */
17
+ export const UPDATE_ACTIVATION_CONTRACT = "activate-v1";
18
+ export const UPDATE_ACTIVATION_ENTRY = "bin/activate.js";
19
+ /** Manifest field listing the activation contracts an installed tree serves. */
20
+ export const UPDATE_ACTIVATION_MANIFEST_FIELD = "updateActivationContracts";
21
+ const ACTIVATION_DIAGNOSTIC_LIMIT = 2_000;
22
+ /**
23
+ * Activate the installed tree at `packageRoot` with this process's own release code:
24
+ * materialize it into the immutable store, certify, warm, verify supervision, and only
25
+ * then move the active reference. This is what the installed tree runs for itself through
26
+ * its activation entry, and what an updater runs in-process for a tree older than the contract.
27
+ */
28
+ export async function activateInstalledRelease(request, callbacks) {
29
+ const stateStore = new CohortStateStore(request.dataDir);
30
+ let total = 0;
31
+ let completed = 0;
32
+ const candidate = await materializeRelease(request.packageRoot, request.dataDir, {
33
+ onProgress: event => {
34
+ total = event.fileCount;
35
+ callbacks.onMaterializing?.({ completed, total });
36
+ },
37
+ onOperation: event => {
38
+ if (event.operation !== "candidate-write" && event.operation !== "layer-write")
39
+ return;
40
+ completed += 1;
41
+ callbacks.onMaterializing?.({ completed, total });
42
+ },
43
+ });
44
+ if (candidate.packageVersion !== request.targetVersion)
45
+ throw new Error(`installed ${PRODUCT_TEXT.displayName} version ${candidate.packageVersion} does not match target ${request.targetVersion}`);
46
+ await stateStore.recordCandidate(candidate);
47
+ await callbacks.phase("materialized");
48
+ const diagnostics = await certifyMaterializedRelease(candidate, request.dataDir);
49
+ await stateStore.approve(candidate.releaseId, diagnostics);
50
+ await callbacks.phase("certified");
51
+ callbacks.onWarmup?.("started");
52
+ await warmMaterializedRelease(candidate, request.environment);
53
+ callbacks.onWarmup?.("completed");
54
+ await ensureSupervisor(candidate, request.environment);
55
+ // Invariant: warmup and authenticated readiness precede changing the active reference.
56
+ await stateStore.activate(candidate.releaseId);
57
+ await callbacks.phase("active-reference-committed");
58
+ }
59
+ /**
60
+ * Which activation contracts the installed tree serves, read from its manifest. A tree
61
+ * without the field, or without a readable manifest, serves none and is activated in-process.
62
+ */
63
+ export async function readActivationContracts(packageRoot, read = path => readFile(path, "utf8")) {
64
+ try {
65
+ const manifest = JSON.parse(await read(resolve(packageRoot, "package.json")));
66
+ const declared = manifest[UPDATE_ACTIVATION_MANIFEST_FIELD];
67
+ return Array.isArray(declared) ? declared.filter((value) => typeof value === "string") : [];
68
+ }
69
+ catch {
70
+ return [];
71
+ }
72
+ }
73
+ /**
74
+ * Drive the installed tree's own activator and relay its progress. The updater passes
75
+ * the tree what the tree cannot know (the data directory, the target it must match) and
76
+ * learns from it only the phases it reached; every path inside the tree is the tree's business.
77
+ */
78
+ export async function delegateActivation(request, callbacks) {
79
+ const entry = resolve(request.packageRoot, UPDATE_ACTIVATION_ENTRY);
80
+ const child = spawn(process.execPath, [entry, "--data-dir", request.dataDir, "--target-version", request.targetVersion], {
81
+ env: request.environment,
82
+ stdio: ["ignore", "pipe", "pipe"],
83
+ windowsHide: true,
84
+ });
85
+ const decoder = new LineFrameDecoder();
86
+ let diagnostics = "";
87
+ let failure = null;
88
+ let completed = false;
89
+ // Concurrency: callbacks are awaited in order so a phase is journaled before the next one is read.
90
+ let relay = Promise.resolve();
91
+ child.stderr?.setEncoding("utf8");
92
+ child.stderr?.on("data", chunk => { diagnostics = `${diagnostics}${chunk}`.slice(-ACTIVATION_DIAGNOSTIC_LIMIT); });
93
+ child.stdout?.on("data", chunk => {
94
+ let events;
95
+ try {
96
+ events = decoder.push(chunk);
97
+ }
98
+ catch (error) {
99
+ failure ??= errorMessage(error);
100
+ return;
101
+ }
102
+ for (const event of events) {
103
+ relay = relay.then(async () => {
104
+ const parsed = parseActivationEvent(event);
105
+ if (!parsed) {
106
+ failure ??= `unrecognized activation event ${JSON.stringify(event).slice(0, 200)}`;
107
+ return;
108
+ }
109
+ if (parsed.event === "materializing")
110
+ callbacks.onMaterializing?.({ completed: parsed.completed, total: parsed.total });
111
+ else if (parsed.event === "phase")
112
+ await callbacks.phase(parsed.phase);
113
+ else if (parsed.event === "warmup")
114
+ callbacks.onWarmup?.(parsed.state);
115
+ else if (parsed.event === "completed")
116
+ completed = true;
117
+ else
118
+ failure ??= parsed.message;
119
+ }).catch(error => { failure ??= errorMessage(error); });
120
+ }
121
+ });
122
+ const exit = await new Promise((resolvePromise, rejectPromise) => {
123
+ child.once("error", rejectPromise);
124
+ child.once("close", (code, signal) => resolvePromise({ code, signal }));
125
+ });
126
+ await relay;
127
+ if (failure)
128
+ throw new Error(failure);
129
+ if (exit.code !== 0 || !completed) {
130
+ const status = exit.code === null ? exit.signal ?? "unknown status" : `status ${exit.code}`;
131
+ throw new Error(`installed release activation exited with ${status}${summarize(diagnostics)}`);
132
+ }
133
+ }
134
+ /**
135
+ * Body of the installed tree's activation entry: activate the tree this code ships in,
136
+ * writing one progress event per line so the updater that started it can follow along.
137
+ */
138
+ export async function runActivationEntry(argv, entryUrl, io = { write: line => process.stdout.write(line) }, environment = process.env) {
139
+ const emit = (event) => io.write(encodeFrame(event));
140
+ try {
141
+ const arguments_ = parseEntryArguments(argv);
142
+ const packageRoot = resolve(fileURLToPath(new URL("..", entryUrl)));
143
+ await activateInstalledRelease({
144
+ packageRoot,
145
+ dataDir: arguments_.dataDir ?? resolveProductPaths(environment).dataDir,
146
+ targetVersion: arguments_.targetVersion,
147
+ environment,
148
+ }, {
149
+ phase: async (phase) => emit({ event: "phase", phase }),
150
+ onMaterializing: progress => emit({ event: "materializing", ...progress }),
151
+ onWarmup: state => emit({ event: "warmup", state }),
152
+ });
153
+ emit({ event: "completed" });
154
+ return 0;
155
+ }
156
+ catch (error) {
157
+ emit({ event: "failed", message: errorMessage(error) });
158
+ return 1;
159
+ }
160
+ }
161
+ function parseEntryArguments(argv) {
162
+ let dataDir = null;
163
+ let targetVersion = null;
164
+ for (let index = 0; index < argv.length; index += 1) {
165
+ const value = argv[index + 1];
166
+ if (argv[index] === "--data-dir" && value !== undefined) {
167
+ dataDir = value;
168
+ index += 1;
169
+ }
170
+ else if (argv[index] === "--target-version" && value !== undefined) {
171
+ targetVersion = value;
172
+ index += 1;
173
+ }
174
+ else
175
+ throw new Error(`unexpected activation argument ${argv[index]}`);
176
+ }
177
+ if (targetVersion === null)
178
+ throw new Error("activation requires --target-version");
179
+ return { dataDir, targetVersion };
180
+ }
181
+ function parseActivationEvent(value) {
182
+ if (typeof value !== "object" || value === null || !("event" in value))
183
+ return null;
184
+ const record = value;
185
+ switch (record.event) {
186
+ case "materializing":
187
+ return typeof record.completed === "number" && typeof record.total === "number" ? { event: "materializing", completed: record.completed, total: record.total } : null;
188
+ case "phase":
189
+ return record.phase === "materialized" || record.phase === "certified" || record.phase === "active-reference-committed" ? { event: "phase", phase: record.phase } : null;
190
+ case "warmup":
191
+ return record.state === "started" || record.state === "completed" ? { event: "warmup", state: record.state } : null;
192
+ case "completed":
193
+ return { event: "completed" };
194
+ case "failed":
195
+ return typeof record.message === "string" ? { event: "failed", message: record.message } : null;
196
+ default:
197
+ return null;
198
+ }
199
+ }
200
+ function summarize(text) {
201
+ const lines = text.split(/\r?\n/).map(line => line.trim()).filter(line => line.length > 0);
202
+ return lines.length === 0 ? "" : `: ${lines.slice(0, 4).join(" | ")}`;
203
+ }
204
+ function errorMessage(error) { return error instanceof Error ? error.message : String(error); }
@@ -1,4 +1,5 @@
1
1
  import type { UpdateChannel } from "./types.js";
2
+ import { type UpdateActivationPhase, type UpdateMaterializationProgress } from "./update-activation.js";
2
3
  import { type UpdateRecoveryState, type UpdateTransaction, type UpdateTransactionPhase } from "./update-transaction.js";
3
4
  import { type ProtectedPackageReplacementResult } from "./update-recovery.js";
4
5
  export declare const PRODUCT_PACKAGE: string;
@@ -9,6 +10,7 @@ export interface ProcessRequest {
9
10
  export interface ProcessResult {
10
11
  code: number | null;
11
12
  stdout: string;
13
+ stderr?: string;
12
14
  }
13
15
  export type UpdateProcessRunner = (command: string, arguments_: readonly string[], request: ProcessRequest) => Promise<ProcessResult>;
14
16
  export interface UpdateFileSystem {
@@ -57,16 +59,7 @@ export interface SelfUpdateOptions {
57
59
  onPhaseTiming?: (event: UpdatePhaseTimingEvent) => void;
58
60
  now?: () => number;
59
61
  }
60
- export type UpdateActivationPhase = Extract<UpdateTransactionPhase, "materialized" | "certified" | "active-reference-committed">;
61
- /**
62
- * Copying the release is the longest step with nothing to say for itself, so it
63
- * reports the files it has written against the files it must write. A caller that
64
- * shows progress can then move with the work instead of guessing at it.
65
- */
66
- export interface UpdateMaterializationProgress {
67
- readonly completed: number;
68
- readonly total: number;
69
- }
62
+ export type { UpdateActivationPhase, UpdateMaterializationProgress } from "./update-activation.js";
70
63
  export interface UpdateLifecycleCoordinator {
71
64
  targetIsActive(targetVersion: string): Promise<boolean>;
72
65
  shutdownVerifiedOwners(targetVersion: string): Promise<{
@@ -103,6 +96,8 @@ export interface UpdatePackageReplacementInput {
103
96
  readonly environment: NodeJS.ProcessEnv;
104
97
  readonly onRecoveryState: (state: UpdateRecoveryState) => Promise<void>;
105
98
  }
99
+ /** Bound on the child diagnostics an update keeps: enough for npm's failure reason, never a log. */
100
+ export declare const CHILD_DIAGNOSTIC_LIMIT = 8000;
106
101
  export declare function createNpmProcessRunner(platform?: NodeJS.Platform): UpdateProcessRunner;
107
102
  export type UpdateOwnershipAction = "clean-dead-record" | "leave-running" | "end-session";
108
103
  /**
@@ -13,7 +13,7 @@ import { CohortStateStore } from "./cohort-state.js";
13
13
  import { cleanupVerifiedOwner, processIsAlive } from "./process-cleanup.js";
14
14
  import { materializeRelease, readMaterializedRelease } from "./release-store.js";
15
15
  import { scheduleReleaseCleanup } from "./release-gc.js";
16
- import { warmMaterializedRelease } from "./warmup.js";
16
+ import { UPDATE_ACTIVATION_CONTRACT, activateInstalledRelease, delegateActivation, readActivationContracts, } from "./update-activation.js";
17
17
  import { UpdateTransactionStore } from "./update-transaction.js";
18
18
  import { removeUpdateRecoveryCapsule, runProtectedPackageReplacement } from "./update-recovery.js";
19
19
  export const PRODUCT_PACKAGE = PRODUCT_TEXT.packageName;
@@ -27,12 +27,19 @@ const defaultOutput = {
27
27
  stdout(message) { process.stdout.write(message); },
28
28
  stderr(message) { process.stderr.write(message); },
29
29
  };
30
+ /** Bound on the child diagnostics an update keeps: enough for npm's failure reason, never a log. */
31
+ export const CHILD_DIAGNOSTIC_LIMIT = 8_000;
30
32
  export function createNpmProcessRunner(platform = process.platform) {
31
33
  return async (command, arguments_, request) => await new Promise((resolvePromise, rejectPromise) => {
32
- const stdio = request.captureStdout ? ["ignore", "pipe", "inherit"] : ["inherit", "inherit", "inherit"];
34
+ // Invariant: no child of the updater shares the terminal. The progress bar is the only
35
+ // live output; a child's own text is kept, bounded, and shown with a failure or not at all.
36
+ const stdio = ["ignore", request.captureStdout ? "pipe" : "ignore", "pipe"];
33
37
  const child = platform === "win32" ? crossSpawn(command, [...arguments_], { stdio }) : spawn(command, [...arguments_], { stdio });
34
38
  const stdout = [];
39
+ let stderr = "";
35
40
  child.stdout?.on("data", chunk => stdout.push(Buffer.from(chunk)));
41
+ child.stderr?.setEncoding("utf8");
42
+ child.stderr?.on("data", chunk => { stderr = `${stderr}${chunk}`.slice(-CHILD_DIAGNOSTIC_LIMIT); });
36
43
  let settled = false;
37
44
  child.once("error", error => { if (!settled) {
38
45
  settled = true;
@@ -40,7 +47,7 @@ export function createNpmProcessRunner(platform = process.platform) {
40
47
  } });
41
48
  child.once("close", code => { if (!settled) {
42
49
  settled = true;
43
- resolvePromise({ code, stdout: Buffer.concat(stdout).toString("utf8") });
50
+ resolvePromise({ code, stdout: Buffer.concat(stdout).toString("utf8"), stderr });
44
51
  } });
45
52
  });
46
53
  }
@@ -143,34 +150,20 @@ export function createUpdateLifecycleCoordinator(environment = process.env, file
143
150
  }
144
151
  },
145
152
  async activateInstalled(packageRoot, targetVersion, phase, onMaterializing, onWarmup) {
146
- let total = 0;
147
- let completed = 0;
148
- const candidate = await materializeRelease(packageRoot, paths.dataDir, {
149
- onProgress: event => {
150
- total = event.fileCount;
151
- onMaterializing?.({ completed, total });
152
- },
153
- onOperation: event => {
154
- if (event.operation !== "candidate-write" && event.operation !== "layer-write")
155
- return;
156
- completed += 1;
157
- onMaterializing?.({ completed, total });
158
- },
159
- });
160
- if (candidate.packageVersion !== targetVersion)
161
- throw new Error(`installed ${PRODUCT_TEXT.displayName} version ${candidate.packageVersion} does not match target ${targetVersion}`);
162
- await stateStore.recordCandidate(candidate);
163
- await phase("materialized");
164
- const diagnostics = await certifyMaterializedRelease(candidate, paths.dataDir);
165
- await stateStore.approve(candidate.releaseId, diagnostics);
166
- await phase("certified");
167
- onWarmup?.("started");
168
- await warmMaterializedRelease(candidate, environment);
169
- onWarmup?.("completed");
170
- await ensureSupervisor(candidate, environment);
171
- // Invariant: warmup and authenticated readiness precede changing the active reference.
172
- await stateStore.activate(candidate.releaseId);
173
- await phase("active-reference-committed");
153
+ const request = { packageRoot, dataDir: paths.dataDir, targetVersion, environment };
154
+ const callbacks = {
155
+ phase,
156
+ ...(onMaterializing ? { onMaterializing } : {}),
157
+ ...(onWarmup ? { onWarmup } : {}),
158
+ };
159
+ // Compatibility: the tree npm just installed is newer than this updater, so its layout is
160
+ // its own to know. A tree that serves the contract activates itself with its own code;
161
+ // a tree from before the contract is activated here, with the layout it had then.
162
+ const contracts = await readActivationContracts(packageRoot, fileSystem.readFile);
163
+ if (contracts.includes(UPDATE_ACTIVATION_CONTRACT))
164
+ await delegateActivation(request, callbacks);
165
+ else
166
+ await activateInstalledRelease(request, callbacks);
174
167
  },
175
168
  };
176
169
  }
@@ -444,10 +437,10 @@ export async function runSelfUpdate(options) {
444
437
  transaction = await transactionStore.setRecovery(recovery) ?? transaction;
445
438
  },
446
439
  }));
447
- if (replacement.stdout.trim().length > 0 && replacement.outcome !== "installed")
448
- output.stderr(`${replacement.stdout.trimEnd()}\n`);
449
- if (replacement.stderr.trim().length > 0)
450
- output.stderr(`${replacement.stderr.trimEnd()}\n`);
440
+ // Rationale: npm's own text explains a failed replacement and says nothing about a
441
+ // successful one, so it reaches the terminal only when the installation did not happen.
442
+ if (replacement.outcome !== "installed")
443
+ reportChildDiagnostics(output, replacement);
451
444
  if (replacement.outcome === "installed")
452
445
  transaction = await transactionStore.advance("package-installed");
453
446
  if (replacement.cancelled) {
@@ -464,8 +457,7 @@ export async function runSelfUpdate(options) {
464
457
  if (installation.result === null)
465
458
  throw new UpdateFailure(installation.exitCode, "npm process failed");
466
459
  if (installation.result.code !== 0) {
467
- if (installation.result.stdout.trim().length > 0)
468
- output.stderr(`${installation.result.stdout.trimEnd()}\n`);
460
+ reportChildDiagnostics(output, installation.result);
469
461
  throw new UpdateFailure(unsuccessfulCode(installation.result.code), `npm exited with status ${formatExitCode(installation.result.code)}`);
470
462
  }
471
463
  transaction = await transactionStore.advance("package-installed");
@@ -581,11 +573,20 @@ async function runNpm(runner, arguments_, captureStdout, output, action, reportN
581
573
  return { result: null, exitCode: 1 };
582
574
  }
583
575
  if (result.code !== 0 && reportNonzero) {
584
- output.stderr(`${PRODUCT_TEXT.diagnostic(`could not ${action}; npm exited with status ${formatExitCode(result.code)}. Review npm's diagnostics above.`)}\n`);
576
+ const explained = reportChildDiagnostics(output, result);
577
+ output.stderr(`${PRODUCT_TEXT.diagnostic(`could not ${action}; npm exited with status ${formatExitCode(result.code)}.${explained ? " Review npm's diagnostics above." : ""}`)}\n`);
585
578
  return { result: null, exitCode: unsuccessfulCode(result.code) };
586
579
  }
587
580
  return { result, exitCode: 0 };
588
581
  }
582
+ /** Prints what a failed child said, bounded, and reports whether there was anything to print. */
583
+ function reportChildDiagnostics(output, result) {
584
+ const text = [result.stdout, result.stderr ?? ""].map(part => part.trim()).filter(part => part.length > 0).join("\n").slice(-CHILD_DIAGNOSTIC_LIMIT);
585
+ if (text.length === 0)
586
+ return false;
587
+ output.stderr(`${text}\n`);
588
+ return true;
589
+ }
589
590
  async function rollbackPriorCohort(dataDir, environment, priorReleaseId) {
590
591
  if (!priorReleaseId)
591
592
  return "no prior cohort was available for rollback";
@@ -5,7 +5,7 @@
5
5
  "platform": "darwin",
6
6
  "architecture": "arm64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-19T12:23:45.099Z",
8
+ "builtAt": "2026-09-19T13:30:30.983Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "9db1726bbe3fc2e8292f2ead1e7e9d0fd4d7d0dc9217b372582bf354b0f565dc",
@@ -5,7 +5,7 @@
5
5
  "platform": "linux",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-19T12:23:39.291Z",
8
+ "builtAt": "2026-09-19T13:30:39.829Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "d8cda6b0c7cb36c0cc41e90802aceebf6c02eaebbc08a83d7d963d2e918dbd7a",
@@ -5,10 +5,10 @@
5
5
  "platform": "win32",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-19T12:24:18.124Z",
8
+ "builtAt": "2026-09-19T13:31:06.678Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian.exe",
11
- "sha256": "94769c4c9aeece0be7e6f54733782c37e18e780acabf7a4bcba6886e7f9b0b87",
11
+ "sha256": "39cc0b1caeb6d009b118f40db5a66aa58d84e7f6b33443e97670bd3708a83a0e",
12
12
  "size": 177664
13
13
  },
14
14
  "provenance": {
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "schema": "a1-runtime-payload-v1",
3
3
  "entryPoints": [
4
+ "bin/activate.js",
4
5
  "bin/cli.js",
5
6
  "bin/guardian.js",
6
7
  "bin/supervisor.js",
@@ -77,6 +77,8 @@ An update is performed by the release that is already installed, so the contract
77
77
 
78
78
  Metadata that predates contract declaration is stale, never fatal. A pre-cutover active record does not stop a launch: it is ignored, the installed payload is materialized, and ordinary cohort selection activates it. A launch that cannot take the installed payload at all — an installation being replaced right now — starts the retained active release instead of failing, and leaves activation to the next launch. What still requires `neutral-launch-v1` outright is narrow and never blocks a launch: the installed package's own manifest must declare it, and an update recovery capsule is never translated.
79
79
 
80
+ Activation after the global installation is handed over the same way. The package manifest declares `updateActivationContracts` (`activate-v1`) and ships `bin/activate.js`; an updater that serves a declared contract starts that entry with the data directory and the target version and relays its line-delimited progress events (`materializing`, `phase`, `warmup`, `completed`, `failed`) into its own journal and progress bar, while the entry materializes, certifies, warms, and supervises the tree it ships in with that tree's own code. The updater resolves nothing inside the installed tree but `package.json` and that entry; `test/repository-governance/update-activation-contract.test.ts` pins this. A tree that declares no contract the updater serves is activated in-process with the layout it had, which keeps an updater older than the contract and a downgrade to an older preview working.
81
+
80
82
  `test/foundation/release/update-predecessor.integration.test.ts` is the gate for this. It installs each of the most recent published releases and drives the candidate through **that release's own** materialization and warmup, because a fixture built from the candidate would only prove the candidate agrees with itself. No reset or migration runs automatically. If disposable state blocks installation, inspect its resolved paths and obtain separate approval before removal.
81
83
 
82
84
  | State | Treatment |
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@timurproko/a1",
3
- "version": "0.1.8-dev.508",
3
+ "version": "0.1.8-dev.512",
4
4
  "description": "Standalone terminal workspace for supervised native and managed agents",
5
5
  "type": "module",
6
6
  "privateLaunchContract": "neutral-launch-v1",
7
+ "updateActivationContracts": [
8
+ "activate-v1"
9
+ ],
7
10
  "packageManager": "npm@11.13.0",
8
11
  "bin": {
9
12
  "a1": "bin/cli.js"