@empire-builder-kit/nx 1.0.0-rc.44 → 1.0.0-rc.46

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 (100) hide show
  1. package/package.json +3 -3
  2. package/src/blueprints/external-sandbox.d.ts +1 -1
  3. package/src/cli/main.d.ts +11 -5
  4. package/src/cli/main.js +88 -29
  5. package/src/container-images.d.ts +1 -1
  6. package/src/container-images.js +1 -1
  7. package/src/developer-agent/generator-planning.js +10 -3
  8. package/src/developer-agent/operation-registry.js +1 -1
  9. package/src/executors/deployment-plan/deployment-plan.js +5 -1
  10. package/src/executors/deployment-plan/schema.json +1 -1
  11. package/src/executors/fleet/fleet.js +4 -0
  12. package/src/executors/fleet/schema.json +1 -1
  13. package/src/executors/github-bootstrap/github-bootstrap.d.ts +3 -0
  14. package/src/executors/github-bootstrap/github-bootstrap.js +28 -0
  15. package/src/executors/github-bootstrap/schema.json +14 -0
  16. package/src/executors/local-deploy/federated-credential-transport.d.ts +4 -3
  17. package/src/executors/local-deploy/federated-credential-transport.js +15 -5
  18. package/src/foundation/cost-estimate.d.ts +8 -3
  19. package/src/foundation/cost-estimate.js +42 -19
  20. package/src/foundation/index.d.ts +1 -0
  21. package/src/foundation/index.js +1 -0
  22. package/src/foundation/local-private-smoke-access.js +6 -0
  23. package/src/foundation/logging.d.ts +3 -1
  24. package/src/foundation/logging.js +8 -1
  25. package/src/foundation/observability.d.ts +42 -0
  26. package/src/foundation/observability.js +181 -0
  27. package/src/foundation/preflight.js +1 -1
  28. package/src/generators/function/function.js +5 -2
  29. package/src/generators/observability-infrastructure.d.ts +2 -0
  30. package/src/generators/observability-infrastructure.js +31 -0
  31. package/src/generators/observability-policy.d.ts +4 -0
  32. package/src/generators/observability-policy.js +23 -0
  33. package/src/generators/preset/dependency-overrides.d.ts +17 -6
  34. package/src/generators/preset/dependency-overrides.js +17 -6
  35. package/src/generators/preset/tool-versions.d.ts +2 -2
  36. package/src/generators/preset/tool-versions.js +2 -2
  37. package/src/generators/preset/workspace-files-adopter-skills.js +28 -17
  38. package/src/generators/preset/workspace-files-agent-development.d.ts +9 -0
  39. package/src/generators/preset/workspace-files-agent-development.js +42 -0
  40. package/src/generators/preset/workspace-files-thin-deploy.js +17 -4
  41. package/src/generators/preset/workspace-files-thin-docs.js +10 -22
  42. package/src/generators/preset/workspace-files.d.ts +1 -0
  43. package/src/generators/preset/workspace-files.js +120 -35
  44. package/src/generators/slice/built-in-plan-alarm-identity.d.ts +173 -0
  45. package/src/generators/slice/built-in-plan-alarm-identity.js +202 -0
  46. package/src/generators/slice/built-in-plan-api-contract-files.js +136 -0
  47. package/src/generators/slice/built-in-plan-data-files.js +10 -3
  48. package/src/generators/slice/built-in-plan-database-lifecycle-files.js +1 -1
  49. package/src/generators/slice/built-in-plan-event-contract-literal.d.ts +6 -0
  50. package/src/generators/slice/built-in-plan-event-contract-literal.js +52 -0
  51. package/src/generators/slice/built-in-plan-infrastructure-files.js +304 -144
  52. package/src/generators/slice/built-in-plan-key-value-retention-infrastructure.js +2 -2
  53. package/src/generators/slice/built-in-plan-metadata-files.js +37 -2
  54. package/src/generators/slice/built-in-plan-operations-files.js +106 -6
  55. package/src/generators/slice/built-in-plan-project-files.js +1 -0
  56. package/src/generators/slice/built-in-plan-release-files.js +2 -44
  57. package/src/generators/slice/built-in-plan-reliability-files.js +222 -236
  58. package/src/generators/slice/built-in-plan-shared.d.ts +29 -3
  59. package/src/generators/slice/built-in-plan-shared.js +29 -2
  60. package/src/generators/slice/built-in-plan-verification-files.js +0 -1
  61. package/src/generators/slice/built-in-plan-web-files.js +22 -6
  62. package/src/generators/slice/built-in-plan.js +2 -2
  63. package/src/generators/slice/slice.js +2 -0
  64. package/src/generators/upgrade/upgrade.d.ts +4 -3
  65. package/src/generators/upgrade/upgrade.js +139 -29
  66. package/src/generators/workload/frozen-container.d.ts +1 -1
  67. package/src/generators/workload/reliability-registration.d.ts +1 -1
  68. package/src/generators/workload/reliability-registration.js +9 -9
  69. package/src/generators/workload/workload.d.ts +1 -1
  70. package/src/generators/workload/workload.js +32 -10
  71. package/src/git-policy/github-bootstrap.d.ts +18 -0
  72. package/src/git-policy/github-bootstrap.js +99 -6
  73. package/src/local-dev/dev-command-core.d.ts +24 -3
  74. package/src/local-dev/dev-command-core.js +124 -12
  75. package/src/local-dev/errors.d.ts +19 -1
  76. package/src/local-dev/errors.js +18 -1
  77. package/src/local-dev/fleet-client.d.ts +64 -6
  78. package/src/local-dev/fleet-client.js +189 -15
  79. package/src/local-dev/fleet-process.d.ts +47 -2
  80. package/src/local-dev/fleet-process.js +248 -31
  81. package/src/local-dev/fleet-runtime.d.ts +37 -0
  82. package/src/local-dev/fleet-runtime.js +127 -69
  83. package/src/local-dev/fleet-session.d.ts +1 -0
  84. package/src/local-dev/fleet-session.js +83 -7
  85. package/src/local-dev/fleet-state.d.ts +5 -0
  86. package/src/local-dev/fleet-state.js +11 -2
  87. package/src/local-dev/fleet-supervisor-daemon.js +1 -5
  88. package/src/local-dev/fleet-supervisor.d.ts +7 -0
  89. package/src/local-dev/fleet-supervisor.js +83 -12
  90. package/src/local-dev/slice-runner.d.ts +19 -1
  91. package/src/local-dev/slice-runner.js +290 -91
  92. package/src/repository-policy/deployment-plan.d.ts +1 -1
  93. package/src/repository-policy/deployment-plan.js +16 -1
  94. package/src/secrets/generated-aws-control-plane.d.ts +11 -1
  95. package/src/secrets/generated-aws-control-plane.js +92 -29
  96. package/src/secrets/operator.d.ts +3 -2
  97. package/src/secrets/operator.js +27 -5
  98. package/src/supply-chain/blueprint-verification-authority.js +24 -3
  99. package/src/supply-chain/generated-files.js +9 -0
  100. package/src/supply-chain/sbom.js +74 -12
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.resolveDevelopmentRouter = resolveDevelopmentRouter;
4
+ exports.createFleetSliceRunnerEffects = createFleetSliceRunnerEffects;
4
5
  exports.startFleetRuntime = startFleetRuntime;
5
6
  const node_fs_1 = require("node:fs");
6
7
  const node_crypto_1 = require("node:crypto");
@@ -71,6 +72,111 @@ async function resolveDevelopmentRouter(request) {
71
72
  throw new errors_js_1.LocalDevContractError('foundation-unavailable', `Development Foundation discovery failed: ${error instanceof Error ? error.message : String(error)}`);
72
73
  }
73
74
  }
75
+ /** One Slice's Fleet process effects: a coordinating runner per launch,
76
+ * generation-bound restart and quiescence, and cleanup whose retry output
77
+ * joins the Slice log store. startFleetRuntime and the real-runner
78
+ * quiescence specs share this wiring, so a dropped effect fails those specs. */
79
+ function createFleetSliceRunnerEffects(input, daemon = {}) {
80
+ const { logStore, name } = input;
81
+ let activeRunner;
82
+ let activeAttempt = 0;
83
+ const runnersByChild = new WeakMap();
84
+ const project = input.projects[name];
85
+ const nativeProjectRoot = project?.targets?.[input.adapter.target]?.executor ===
86
+ '@empire-builder-kit/nx:dev'
87
+ ? (0, node_path_1.join)(input.workspaceRealpath, project.root)
88
+ : undefined;
89
+ const effects = {
90
+ ...(nativeProjectRoot
91
+ ? {
92
+ nativeDeployment: async (signal) => activeRunner
93
+ ? (0, native_completion_js_1.readNativeDevelopmentCompletion)({
94
+ projectRoot: nativeProjectRoot,
95
+ stage: input.stage,
96
+ startedAt: activeRunner.request.createdAt,
97
+ }, signal)
98
+ : 'pending',
99
+ applicationStarted: () => Boolean(activeRunner &&
100
+ (0, fleet_web_start_js_1.hasFleetWebStarted)(activeRunner.requestPath, activeRunner.request)),
101
+ }
102
+ : {}),
103
+ cleanup: async () => {
104
+ if (!activeRunner) {
105
+ throw new errors_js_1.LocalDevContractError('cleanup-incomplete', `Fleet Slice runner state for ${name} is missing.`);
106
+ }
107
+ try {
108
+ (0, slice_runner_js_1.assertFleetSliceRunnerCleanup)(activeRunner.statePath, activeRunner.request);
109
+ }
110
+ catch {
111
+ // A runner that already exited (or whose own cleanup failed)
112
+ // is retried by a separate runner; keep its output in the
113
+ // same Slice log as the in-runner cleanup.
114
+ const attempt = activeAttempt;
115
+ try {
116
+ await (0, slice_runner_js_1.retryFleetSliceRunnerCleanup)(activeRunner, {
117
+ ...daemon,
118
+ output: (stream, chunk) => {
119
+ logStore.writeChunk(name, stream, attempt, chunk);
120
+ },
121
+ });
122
+ }
123
+ finally {
124
+ logStore.flush(name, undefined, attempt);
125
+ }
126
+ (0, slice_runner_js_1.assertFleetSliceRunnerCleanup)(activeRunner.statePath, activeRunner.request);
127
+ }
128
+ await (0, slice_runner_js_1.recoverFleetSliceRunnerCleanupsForSlices)({
129
+ fleetId: activeRunner.request.fleetId,
130
+ sessionGeneration: activeRunner.request.sessionGeneration,
131
+ }, [name], input.runtimePaths, daemon);
132
+ },
133
+ launch: (attempt) => {
134
+ activeAttempt = attempt;
135
+ activeRunner = (0, slice_runner_js_1.launchFleetSliceRunner)({
136
+ origin: input.origin,
137
+ parent: input.parent,
138
+ port: input.port,
139
+ runnerGeneration: `${input.sessionGeneration}:${name}:${attempt}:${(0, node_crypto_1.randomUUID)()}`,
140
+ runtimePaths: input.runtimePaths,
141
+ sessionGeneration: input.sessionGeneration,
142
+ slice: name,
143
+ startupPath: input.startupPath,
144
+ workspaceRealpath: input.workspaceRealpath,
145
+ }, daemon);
146
+ runnersByChild.set(activeRunner.child, activeRunner);
147
+ return { attempt, child: activeRunner.child };
148
+ },
149
+ prepareRestart: async () => {
150
+ if (!activeRunner) {
151
+ throw new errors_js_1.LocalDevContractError('cleanup-incomplete', `Fleet Slice runner state for ${name} is missing.`);
152
+ }
153
+ (0, slice_runner_js_1.requestFleetSliceRunnerRestart)(activeRunner);
154
+ },
155
+ // The runner stops its writer and stays alive with pending
156
+ // cleanup authority; a later stop runs declared cleanup in that
157
+ // runner, so its output reaches this Slice's log store.
158
+ quiesce: async (child) => {
159
+ if (!activeRunner || activeRunner.child !== child) {
160
+ throw new errors_js_1.LocalDevContractError('cleanup-incomplete', 'Fleet runner ownership changed before quiescence.');
161
+ }
162
+ await (0, fleet_process_js_1.quiesceCoordinatingFleetRunner)(child, activeRunner.request.runnerGeneration);
163
+ },
164
+ runnerGeneration: () => activeRunner?.request.runnerGeneration,
165
+ startTargetExit: (child) => {
166
+ const runner = runnersByChild.get(child);
167
+ return runner
168
+ ? (0, slice_runner_js_1.readFleetSliceRunnerState)(runner.statePath).childExit
169
+ : undefined;
170
+ },
171
+ terminate: async (child) => {
172
+ if (!activeRunner || activeRunner.child !== child) {
173
+ throw new errors_js_1.LocalDevContractError('cleanup-incomplete', 'Fleet runner ownership changed before shutdown.');
174
+ }
175
+ await (0, fleet_process_js_1.terminateCoordinatingFleetRunner)(child, activeRunner.request.runnerGeneration);
176
+ },
177
+ };
178
+ return { activeRunner: () => activeRunner, effects };
179
+ }
74
180
  async function startFleetRuntime(startupPath, dependencies = {}) {
75
181
  const request = (0, fleet_startup_js_1.readFleetStartupRequest)(startupPath);
76
182
  if ((0, node_fs_1.realpathSync)(request.workspaceRealpath) !== request.workspaceRealpath) {
@@ -220,75 +326,27 @@ async function startFleetRuntime(startupPath, dependencies = {}) {
220
326
  adapters: plan.adapters,
221
327
  allocatePort: async (name) => ports.get(name),
222
328
  fleetId,
223
- processFactory: ({ adapter, name, onStateChange, upstream }) => {
224
- let activeRunner;
225
- return new fleet_process_js_1.FleetProcessController({
226
- adapter,
227
- logStore,
228
- maxRestartAttempts: request.context.maxRestartAttempts,
229
- name,
230
- onStateChange,
231
- upstream,
232
- }, {
233
- ...(plan.projects[name]?.targets?.[adapter.target]?.executor ===
234
- '@empire-builder-kit/nx:dev'
235
- ? {
236
- nativeDeployment: async (signal) => activeRunner
237
- ? (0, native_completion_js_1.readNativeDevelopmentCompletion)({
238
- projectRoot: (0, node_path_1.join)(request.workspaceRealpath, plan.projects[name].root),
239
- stage: request.identity.stage,
240
- startedAt: activeRunner.request.createdAt,
241
- }, signal)
242
- : 'pending',
243
- applicationStarted: () => Boolean(activeRunner &&
244
- (0, fleet_web_start_js_1.hasFleetWebStarted)(activeRunner.requestPath, activeRunner.request)),
245
- }
246
- : {}),
247
- cleanup: async () => {
248
- if (!activeRunner) {
249
- throw new errors_js_1.LocalDevContractError('cleanup-incomplete', `Fleet Slice runner state for ${name} is missing.`);
250
- }
251
- try {
252
- (0, slice_runner_js_1.assertFleetSliceRunnerCleanup)(activeRunner.statePath, activeRunner.request);
253
- }
254
- catch {
255
- await (0, slice_runner_js_1.retryFleetSliceRunnerCleanup)(activeRunner);
256
- (0, slice_runner_js_1.assertFleetSliceRunnerCleanup)(activeRunner.statePath, activeRunner.request);
257
- }
258
- await (0, slice_runner_js_1.recoverFleetSliceRunnerCleanupsForSlices)({
259
- fleetId: activeRunner.request.fleetId,
260
- sessionGeneration: activeRunner.request.sessionGeneration,
261
- }, [name], runtimePaths);
262
- },
263
- launch: (attempt) => {
264
- activeRunner = (0, slice_runner_js_1.launchFleetSliceRunner)({
265
- origin,
266
- parent: processOwner,
267
- port: ports.get(name),
268
- runnerGeneration: `${request.sessionGeneration}:${name}:${attempt}:${(0, node_crypto_1.randomUUID)()}`,
269
- runtimePaths,
270
- sessionGeneration: request.sessionGeneration,
271
- slice: name,
272
- startupPath,
273
- workspaceRealpath: request.workspaceRealpath,
274
- });
275
- return { attempt, child: activeRunner.child };
276
- },
277
- prepareRestart: async () => {
278
- if (!activeRunner) {
279
- throw new errors_js_1.LocalDevContractError('cleanup-incomplete', `Fleet Slice runner state for ${name} is missing.`);
280
- }
281
- (0, slice_runner_js_1.requestFleetSliceRunnerRestart)(activeRunner);
282
- },
283
- runnerGeneration: () => activeRunner?.request.runnerGeneration,
284
- terminate: async (child) => {
285
- if (!activeRunner || activeRunner.child !== child) {
286
- throw new errors_js_1.LocalDevContractError('cleanup-incomplete', 'Fleet runner ownership changed before shutdown.');
287
- }
288
- await (0, fleet_process_js_1.terminateCoordinatingFleetRunner)(child, activeRunner.request.runnerGeneration);
289
- },
290
- });
291
- },
329
+ processFactory: ({ adapter, name, onStateChange, upstream }) => new fleet_process_js_1.FleetProcessController({
330
+ adapter,
331
+ logStore,
332
+ maxRestartAttempts: request.context.maxRestartAttempts,
333
+ name,
334
+ onStateChange,
335
+ upstream,
336
+ }, createFleetSliceRunnerEffects({
337
+ adapter,
338
+ logStore,
339
+ name,
340
+ origin,
341
+ parent: processOwner,
342
+ port: ports.get(name),
343
+ projects: plan.projects,
344
+ runtimePaths,
345
+ sessionGeneration: request.sessionGeneration,
346
+ stage: request.identity.stage,
347
+ startupPath,
348
+ workspaceRealpath: request.workspaceRealpath,
349
+ }).effects),
292
350
  proxy: proxyHost,
293
351
  proxyPort,
294
352
  routes,
@@ -8,6 +8,7 @@ export interface FleetSliceProcess {
8
8
  readonly runnerGeneration?: string;
9
9
  readonly state: FleetProcessRouteState;
10
10
  interruptStartup?(): void;
11
+ quiesce(): Promise<void>;
11
12
  restart(): Promise<void>;
12
13
  start(): Promise<void>;
13
14
  stop(options: {
@@ -8,6 +8,12 @@ const slice_claim_js_1 = require("./slice-claim.js");
8
8
  function sessionError(classification, message) {
9
9
  throw new errors_js_1.LocalDevContractError(classification, message);
10
10
  }
11
+ function startupFailure(error) {
12
+ const message = error instanceof Error ? error.message : String(error);
13
+ return error instanceof errors_js_1.LocalDevContractError
14
+ ? `${error.classification}: ${message}`
15
+ : message;
16
+ }
11
17
  class FleetSession {
12
18
  routes;
13
19
  #allocatePort;
@@ -16,6 +22,7 @@ class FleetSession {
16
22
  #cleanupStates = new Map();
17
23
  #fleetId;
18
24
  #operation = Promise.resolve();
25
+ #pendingStartup;
19
26
  #processFactory;
20
27
  #processes = new Map();
21
28
  #proxy;
@@ -79,6 +86,29 @@ class FleetSession {
79
86
  this.#operation = result.then(() => undefined, () => undefined);
80
87
  return result;
81
88
  }
89
+ async #quiesce(names) {
90
+ const failures = [];
91
+ // The complete selection was dependency-validated before this phase.
92
+ // Keep every claim selected until all writers stop, even if one fails.
93
+ for (const name of names) {
94
+ const process = this.#processes.get(name);
95
+ if (!process)
96
+ continue;
97
+ const route = this.#route(name);
98
+ try {
99
+ await process.quiesce();
100
+ }
101
+ catch (error) {
102
+ this.#cleanupStates.set(name, 'failed');
103
+ failures.push(`${name}: ${error instanceof Error ? error.message : String(error)}`);
104
+ }
105
+ finally {
106
+ route.selected = true;
107
+ route.state = 'unavailable';
108
+ }
109
+ }
110
+ return failures;
111
+ }
82
112
  up(names) {
83
113
  return this.#serialized(async () => {
84
114
  const requested = (0, contracts_js_1.resolveFleetSliceOperation)(this.#adapters, 'up', names, this.selectedSlices());
@@ -115,15 +145,30 @@ class FleetSession {
115
145
  upstream: route.upstream,
116
146
  });
117
147
  this.#processes.set(name, process);
118
- await process.start();
148
+ const pending = {
149
+ name,
150
+ process,
151
+ interruptionRequested: false,
152
+ };
153
+ this.#pendingStartup = pending;
154
+ try {
155
+ await process.start();
156
+ }
157
+ finally {
158
+ if (this.#pendingStartup === pending)
159
+ this.#pendingStartup = undefined;
160
+ }
119
161
  }
120
162
  }
121
163
  catch (error) {
122
- const cleanupFailures = [];
164
+ const quiesceFailures = await this.#quiesce([...changed].reverse());
165
+ const cleanupFailures = [...quiesceFailures];
123
166
  const releasable = [];
124
167
  for (const name of [...changed].reverse()) {
125
168
  const process = this.#processes.get(name);
126
169
  if (process) {
170
+ if (quiesceFailures.length > 0)
171
+ continue;
127
172
  try {
128
173
  (0, contracts_js_1.resolveFleetSliceOperation)(this.#adapters, 'down', [name], this.selectedSlices());
129
174
  await process.stop({ cleanup: true });
@@ -161,7 +206,9 @@ class FleetSession {
161
206
  }
162
207
  }
163
208
  if (cleanupFailures.length > 0) {
164
- return sessionError('cleanup-incomplete', `Fleet startup failed and generation-owned rollback was incomplete: ${cleanupFailures.join('; ')}`);
209
+ // The start failure leads: the persisted failure message is
210
+ // bounded, and the cause must survive the rollback report.
211
+ return sessionError('cleanup-incomplete', `Fleet startup failed (${startupFailure(error)}) and generation-owned rollback was incomplete: ${cleanupFailures.join('; ')}`);
165
212
  }
166
213
  throw error;
167
214
  }
@@ -194,7 +241,28 @@ class FleetSession {
194
241
  return sessionError('not-running', `Product Slice ${name} is not selected.`);
195
242
  }
196
243
  const previousState = route.state;
197
- await process.restart();
244
+ const pending = {
245
+ name,
246
+ process,
247
+ interruptionRequested: false,
248
+ };
249
+ this.#pendingStartup = pending;
250
+ try {
251
+ await process.restart();
252
+ }
253
+ catch (error) {
254
+ if (pending.interruptionRequested) {
255
+ const failures = await this.#quiesce([name]);
256
+ if (failures.length > 0) {
257
+ return sessionError('cleanup-incomplete', `Fleet restart failed (${startupFailure(error)}) and owned writer quiescence was incomplete: ${failures.join('; ')}`);
258
+ }
259
+ }
260
+ throw error;
261
+ }
262
+ finally {
263
+ if (this.#pendingStartup === pending)
264
+ this.#pendingStartup = undefined;
265
+ }
198
266
  results.push({
199
267
  changed: true,
200
268
  name,
@@ -214,10 +282,15 @@ class FleetSession {
214
282
  const results = [];
215
283
  const cleanupFailures = [];
216
284
  const ordered = (0, contracts_js_1.resolveFleetSliceOperation)(this.#adapters, 'down', names, this.selectedSlices());
285
+ const previousStates = new Map(ordered.map((name) => [name, this.#route(name).state]));
286
+ const quiesceFailures = await this.#quiesce(ordered);
287
+ if (quiesceFailures.length > 0) {
288
+ return sessionError('cleanup-incomplete', `Fleet writers did not quiesce; no cloud cleanup ran and all claims were retained: ${quiesceFailures.join('; ')}`);
289
+ }
217
290
  for (const name of ordered) {
218
291
  const route = this.#route(name);
219
292
  const process = this.#processes.get(name);
220
- const previousState = route.state;
293
+ const previousState = previousStates.get(name);
221
294
  if (!route.selected || !process) {
222
295
  results.push({
223
296
  changed: false,
@@ -275,8 +348,11 @@ class FleetSession {
275
348
  // running up operation owns rollback; down waits for that rollback rather
276
349
  // than racing a second native cleanup against a live writer.
277
350
  const ordered = (0, contracts_js_1.resolveFleetSliceOperation)(this.#adapters, 'down', names, this.selectedSlices());
278
- for (const name of ordered)
279
- this.#processes.get(name)?.interruptStartup?.();
351
+ const pending = this.#pendingStartup;
352
+ if (pending && ordered.includes(pending.name)) {
353
+ pending.interruptionRequested = true;
354
+ pending.process.interruptStartup?.();
355
+ }
280
356
  }
281
357
  allSlices() {
282
358
  return this.routes.map(({ name }) => name).sort();
@@ -5,6 +5,9 @@ import { type PrivateStateSecurity } from './private-state.js';
5
5
  import type { FleetRouterSource } from './fleet-configuration.js';
6
6
  export declare const FLEET_STATE_SCHEMA_VERSION: 1;
7
7
  export declare const FLEET_STATE_FILE: "control-state.json";
8
+ /** A persisted operation failure cause stays one bounded line, so the full
9
+ * state (up to 128 operations) remains inside the CLI result bound. */
10
+ export declare const FLEET_OPERATION_FAILURE_MESSAGE_MAX_LENGTH = 1024;
8
11
  export declare const FLEET_ROUTE_STATES: readonly ["remote", "starting", "ready", "unavailable"];
9
12
  export declare const FLEET_CLEANUP_STATES: readonly ["not-declared", "pending", "succeeded", "failed"];
10
13
  export declare const FLEET_OPERATION_STATES: readonly ["accepted", "running", "succeeded", "failed"];
@@ -28,6 +31,8 @@ export interface FleetOperationStateRecord {
28
31
  command: FleetMutationCommand;
29
32
  completedAt?: string;
30
33
  failureClassification?: LocalDevFailureClassification;
34
+ /** Bounded, redacted, single-line cause of a failed operation. */
35
+ failureMessage?: string;
31
36
  id: string;
32
37
  idempotencyKey: string;
33
38
  slices: string[];
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FLEET_OPERATION_STATES = exports.FLEET_CLEANUP_STATES = exports.FLEET_ROUTE_STATES = exports.FLEET_STATE_FILE = exports.FLEET_STATE_SCHEMA_VERSION = void 0;
3
+ exports.FLEET_OPERATION_STATES = exports.FLEET_CLEANUP_STATES = exports.FLEET_ROUTE_STATES = exports.FLEET_OPERATION_FAILURE_MESSAGE_MAX_LENGTH = exports.FLEET_STATE_FILE = exports.FLEET_STATE_SCHEMA_VERSION = void 0;
4
4
  exports.fleetOwnedSliceNames = fleetOwnedSliceNames;
5
5
  exports.fleetOwnsStartingSlice = fleetOwnsStartingSlice;
6
6
  exports.validateFleetStateRecord = validateFleetStateRecord;
@@ -18,6 +18,9 @@ const private_state_js_1 = require("./private-state.js");
18
18
  const personal_stage_js_1 = require("./personal-stage.js");
19
19
  exports.FLEET_STATE_SCHEMA_VERSION = 1;
20
20
  exports.FLEET_STATE_FILE = 'control-state.json';
21
+ /** A persisted operation failure cause stays one bounded line, so the full
22
+ * state (up to 128 operations) remains inside the CLI result bound. */
23
+ exports.FLEET_OPERATION_FAILURE_MESSAGE_MAX_LENGTH = 1_024;
21
24
  exports.FLEET_ROUTE_STATES = [
22
25
  'remote',
23
26
  'starting',
@@ -120,7 +123,7 @@ function validateSliceState(value) {
120
123
  }
121
124
  function validateOperation(value) {
122
125
  (0, contract_validation_js_1.assertRecord)(value, 'Fleet operation state');
123
- (0, contract_validation_js_1.assertExactProperties)(value, ['acceptedAt', 'command', 'id', 'idempotencyKey', 'slices', 'state'], ['completedAt', 'failureClassification'], 'Fleet operation state');
126
+ (0, contract_validation_js_1.assertExactProperties)(value, ['acceptedAt', 'command', 'id', 'idempotencyKey', 'slices', 'state'], ['completedAt', 'failureClassification', 'failureMessage'], 'Fleet operation state');
124
127
  if (value.command !== 'up' &&
125
128
  value.command !== 'down' &&
126
129
  value.command !== 'restart') {
@@ -157,6 +160,12 @@ function validateOperation(value) {
157
160
  }
158
161
  result.failureClassification = classification;
159
162
  }
163
+ if (value.failureMessage !== undefined) {
164
+ if (result.state !== 'failed') {
165
+ return stateError('Fleet operation failureMessage is valid only for a failed operation.');
166
+ }
167
+ result.failureMessage = (0, contract_validation_js_1.requireBoundedString)(value.failureMessage, 'Fleet operation failureMessage', { maximumLength: exports.FLEET_OPERATION_FAILURE_MESSAGE_MAX_LENGTH });
168
+ }
160
169
  const terminal = result.state === 'succeeded' || result.state === 'failed';
161
170
  if (terminal !== (result.completedAt !== undefined)) {
162
171
  return stateError('Fleet operation terminal state and completedAt must agree.');
@@ -10,11 +10,7 @@ async function runFleetSupervisorDaemon(startupPath) {
10
10
  return;
11
11
  handlingSignal = true;
12
12
  try {
13
- const selected = runtime.session.selectedSlices();
14
- if (selected.length > 0)
15
- await runtime.session.down(selected);
16
- await runtime.session.closeProxyWhenEmpty();
17
- await runtime.supervisor.stopGracefully();
13
+ await runtime.supervisor.stopForSignal();
18
14
  }
19
15
  catch (error) {
20
16
  handlingSignal = false;
@@ -42,6 +42,12 @@ export interface FleetSupervisorOptions {
42
42
  security?: PrivateStateSecurity;
43
43
  shutdownGraceMs?: number;
44
44
  }
45
+ /** Failed operations beyond this many keep their classification only. */
46
+ export declare const FLEET_RETAINED_FAILURE_MESSAGES = 8;
47
+ /** The persisted cause is one redacted, bounded line. Whitespace collapses
48
+ * before redaction because the terminal-safety strip deletes line breaks and
49
+ * the state validator refuses every control character. */
50
+ export declare function fleetOperationFailureMessage(error: unknown): string | undefined;
45
51
  export declare class FleetSupervisor {
46
52
  #private;
47
53
  constructor(options: FleetSupervisorOptions, effects?: FleetSupervisorEffects);
@@ -51,5 +57,6 @@ export declare class FleetSupervisor {
51
57
  waitForIdle(): Promise<void>;
52
58
  waitUntilStopped(): Promise<void>;
53
59
  stopGracefully(): Promise<void>;
60
+ stopForSignal(): Promise<void>;
54
61
  }
55
62
  export {};
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FleetSupervisor = void 0;
3
+ exports.FleetSupervisor = exports.FLEET_RETAINED_FAILURE_MESSAGES = void 0;
4
+ exports.fleetOperationFailureMessage = fleetOperationFailureMessage;
4
5
  const node_crypto_1 = require("node:crypto");
5
6
  const contracts_js_1 = require("./contracts.js");
6
7
  const node_fs_1 = require("node:fs");
@@ -8,6 +9,7 @@ const control_contract_js_1 = require("./control-contract.js");
8
9
  const errors_js_1 = require("./errors.js");
9
10
  const fleet_ipc_js_1 = require("./fleet-ipc.js");
10
11
  const fleet_lock_js_1 = require("./fleet-lock.js");
12
+ const fleet_logs_js_1 = require("./fleet-logs.js");
11
13
  const fleet_state_js_1 = require("./fleet-state.js");
12
14
  const private_state_js_1 = require("./private-state.js");
13
15
  const process_identity_js_1 = require("./process-identity.js");
@@ -33,6 +35,49 @@ function failureClassification(error) {
33
35
  ? error.classification
34
36
  : 'internal-error';
35
37
  }
38
+ /** Failed operations beyond this many keep their classification only. */
39
+ exports.FLEET_RETAINED_FAILURE_MESSAGES = 8;
40
+ /** The persisted cause is one redacted, bounded line. Whitespace collapses
41
+ * before redaction because the terminal-safety strip deletes line breaks and
42
+ * the state validator refuses every control character. */
43
+ function fleetOperationFailureMessage(error) {
44
+ const raw = error instanceof Error ? error.message : String(error);
45
+ const line = (0, fleet_logs_js_1.redactLocalLogText)(raw.replace(/\s+/gu, ' '))
46
+ // The persisted line must carry no control character of any kind.
47
+ // eslint-disable-next-line no-control-regex
48
+ .replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]+/gu, ' ')
49
+ .replace(/ {2,}/gu, ' ')
50
+ .trim();
51
+ if (line.length === 0)
52
+ return undefined;
53
+ if (line.length <= fleet_state_js_1.FLEET_OPERATION_FAILURE_MESSAGE_MAX_LENGTH)
54
+ return line;
55
+ let bounded = '';
56
+ for (const character of line) {
57
+ if (bounded.length + character.length >
58
+ fleet_state_js_1.FLEET_OPERATION_FAILURE_MESSAGE_MAX_LENGTH - 1)
59
+ break;
60
+ bounded += character;
61
+ }
62
+ return `${bounded.trimEnd()}\u2026`;
63
+ }
64
+ function retainRecentFailureMessages(operations) {
65
+ let retained = 0;
66
+ return operations
67
+ .slice()
68
+ .reverse()
69
+ .map((operation) => {
70
+ if (operation.failureMessage === undefined)
71
+ return operation;
72
+ retained += 1;
73
+ if (retained <= exports.FLEET_RETAINED_FAILURE_MESSAGES)
74
+ return operation;
75
+ const classificationOnly = { ...operation };
76
+ delete classificationOnly.failureMessage;
77
+ return classificationOnly;
78
+ })
79
+ .reverse();
80
+ }
36
81
  class FleetSupervisor {
37
82
  #closed = false;
38
83
  #adapters;
@@ -51,6 +96,7 @@ class FleetSupervisor {
51
96
  #shutdownGraceMs;
52
97
  #state;
53
98
  #stopping = false;
99
+ #stopOperation;
54
100
  constructor(options, effects = {}) {
55
101
  this.#adapters = options.adapters;
56
102
  this.#host = options.host;
@@ -316,12 +362,15 @@ class FleetSupervisor {
316
362
  }
317
363
  catch (error) {
318
364
  this.#shutdownAfterOperations.delete(operation.id);
365
+ const failureMessage = fleetOperationFailureMessage(error);
319
366
  this.#state.operations[index] = {
320
367
  ...this.#state.operations[index],
321
368
  completedAt: this.#effects.now().toISOString(),
322
369
  failureClassification: failureClassification(error),
370
+ ...(failureMessage === undefined ? {} : { failureMessage }),
323
371
  state: 'failed',
324
372
  };
373
+ this.#state.operations = retainRecentFailureMessages(this.#state.operations);
325
374
  this.#persistState();
326
375
  }
327
376
  }
@@ -374,22 +423,44 @@ class FleetSupervisor {
374
423
  return;
375
424
  await new Promise((resolvePromise) => this.#closedWaiters.push(resolvePromise));
376
425
  }
377
- async stopGracefully() {
426
+ stopGracefully() {
427
+ return this.#stop('empty');
428
+ }
429
+ stopForSignal() {
430
+ return this.#stop('down-selected');
431
+ }
432
+ #stop(mode) {
378
433
  if (this.#closed)
379
- return;
434
+ return Promise.resolve();
435
+ if (this.#stopOperation)
436
+ return this.#stopOperation;
380
437
  this.#stopping = true;
381
- await this.waitForIdle();
382
- try {
383
- this.#assertShutdownOwnership();
384
- await this.#host.closeProxyWhenEmpty();
385
- (0, fleet_runner_evidence_js_1.retireFleetRunnerEvidence)(this.#state, this.#paths, {
386
- security: this.#security,
387
- });
388
- }
389
- catch (error) {
438
+ this.#stopOperation = this.#finishStop(mode).catch((error) => {
390
439
  this.#stopping = false;
440
+ this.#stopOperation = undefined;
391
441
  throw error;
442
+ });
443
+ return this.#stopOperation;
444
+ }
445
+ async #finishStop(mode) {
446
+ await this.waitForIdle();
447
+ if (mode === 'down-selected') {
448
+ const selected = this.#host.selectedSlices();
449
+ if (selected.length > 0) {
450
+ try {
451
+ this.#syncSliceStates(await this.#host.down(selected));
452
+ }
453
+ finally {
454
+ this.#syncLiveSliceStates();
455
+ this.#persistState();
456
+ }
457
+ }
392
458
  }
459
+ this.#assertShutdownOwnership();
460
+ await this.#host.closeProxyWhenEmpty();
461
+ (0, fleet_runner_evidence_js_1.retireFleetRunnerEvidence)(this.#state, this.#paths, {
462
+ security: this.#security,
463
+ });
393
464
  if (this.#heartbeat)
394
465
  clearInterval(this.#heartbeat);
395
466
  this.#heartbeat = undefined;
@@ -23,8 +23,16 @@ export interface FleetSliceRunnerRequest {
23
23
  startupPath: string;
24
24
  workspaceRealpath: string;
25
25
  }
26
+ /** The Slice start target's own exit when it ended without owned
27
+ * termination: the cause behind a runner that then exits cleanly. */
28
+ export interface FleetSliceRunnerChildExit {
29
+ at: string;
30
+ code: number | null;
31
+ signal: string | null;
32
+ }
26
33
  export interface FleetSliceRunnerState {
27
34
  child?: FleetProcessIdentity;
35
+ childExit?: FleetSliceRunnerChildExit;
28
36
  cleanup: 'failed' | 'pending' | 'running' | 'succeeded';
29
37
  failure?: string;
30
38
  fleetId: string;
@@ -91,6 +99,13 @@ export declare function runnerParentStatus(request: FleetSliceRunnerRequest, opt
91
99
  processProbe?: ProcessIdentityProbe;
92
100
  security?: PrivateStateSecurity;
93
101
  }): 'ambiguous' | 'lost' | 'present';
102
+ interface FleetSliceRunnerControl {
103
+ acknowledgeQuiesce(): void;
104
+ /** Registers the quiesce handler and replays a request that arrived while
105
+ * the runner was still reading its request. */
106
+ onQuiesce(handler: () => void): void;
107
+ stopSignal: AbortSignal;
108
+ }
94
109
  declare function runFleetSliceRunnerCore(requestPath: string, options: {
95
110
  loadPlan?: typeof loadWorkspaceFleetPlan;
96
111
  monitorIntervalMs?: number;
@@ -102,11 +117,14 @@ declare function runFleetSliceRunnerCore(requestPath: string, options: {
102
117
  security?: PrivateStateSecurity;
103
118
  spawnTarget?: typeof spawnDeclaredNxTarget;
104
119
  terminate?: typeof terminateOwnedChild;
105
- }, stopSignal: AbortSignal): Promise<0 | 1>;
120
+ }, control: FleetSliceRunnerControl): Promise<0 | 1>;
106
121
  export declare function runFleetSliceRunner(requestPath: string, options?: Parameters<typeof runFleetSliceRunnerCore>[1]): Promise<0 | 1>;
107
122
  export declare function assertFleetSliceRunnerCleanup(statePath: string, expected: Pick<FleetSliceRunnerRequest, 'fleetId' | 'runnerGeneration' | 'sessionGeneration' | 'slice'>, security?: PrivateStateSecurity): void;
108
123
  export declare function retryFleetSliceRunnerCleanup(launched: Pick<LaunchedFleetSliceRunner, 'request' | 'requestPath' | 'statePath'>, effects?: {
109
124
  daemonPath?: string;
125
+ /** Receives the retry runner's output, including its declared cleanup
126
+ * target's, for the Slice log store. Without it the output is inherited. */
127
+ output?: (stream: 'stderr' | 'stdout', chunk: Buffer | string) => void;
110
128
  processProbe?: (identity: FleetProcessIdentity) => ProcessIdentityProbe;
111
129
  security?: PrivateStateSecurity;
112
130
  spawn?: typeof spawn;