@empire-builder-kit/nx 1.0.0-rc.43 → 1.0.0-rc.45

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 (88) 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/executors/deployment-plan/deployment-plan.js +5 -1
  8. package/src/executors/deployment-plan/schema.json +1 -1
  9. package/src/executors/fleet/fleet.js +4 -0
  10. package/src/executors/fleet/schema.json +1 -1
  11. package/src/executors/local-deploy/federated-credential-transport.d.ts +4 -3
  12. package/src/executors/local-deploy/federated-credential-transport.js +15 -5
  13. package/src/foundation/cost-estimate.d.ts +8 -3
  14. package/src/foundation/cost-estimate.js +42 -19
  15. package/src/foundation/index.d.ts +1 -0
  16. package/src/foundation/index.js +1 -0
  17. package/src/foundation/local-private-smoke-access.js +6 -0
  18. package/src/foundation/logging.d.ts +3 -1
  19. package/src/foundation/logging.js +8 -1
  20. package/src/foundation/observability.d.ts +42 -0
  21. package/src/foundation/observability.js +181 -0
  22. package/src/foundation/preflight.js +1 -1
  23. package/src/generators/function/function.js +5 -2
  24. package/src/generators/observability-infrastructure.d.ts +2 -0
  25. package/src/generators/observability-infrastructure.js +31 -0
  26. package/src/generators/observability-policy.d.ts +4 -0
  27. package/src/generators/observability-policy.js +23 -0
  28. package/src/generators/preset/dependency-overrides.d.ts +34 -0
  29. package/src/generators/preset/dependency-overrides.js +35 -1
  30. package/src/generators/preset/tool-versions.d.ts +2 -2
  31. package/src/generators/preset/tool-versions.js +2 -2
  32. package/src/generators/preset/workspace-files-adopter-skills.js +28 -17
  33. package/src/generators/preset/workspace-files-agent-development.d.ts +9 -0
  34. package/src/generators/preset/workspace-files-agent-development.js +42 -0
  35. package/src/generators/preset/workspace-files-thin-deploy.js +12 -3
  36. package/src/generators/preset/workspace-files-thin-docs.js +10 -22
  37. package/src/generators/preset/workspace-files.d.ts +3 -0
  38. package/src/generators/preset/workspace-files.js +93 -8
  39. package/src/generators/slice/built-in-plan-alarm-identity.d.ts +173 -0
  40. package/src/generators/slice/built-in-plan-alarm-identity.js +202 -0
  41. package/src/generators/slice/built-in-plan-api-contract-files.js +136 -0
  42. package/src/generators/slice/built-in-plan-database-lifecycle-files.js +1 -1
  43. package/src/generators/slice/built-in-plan-event-contract-literal.d.ts +6 -0
  44. package/src/generators/slice/built-in-plan-event-contract-literal.js +52 -0
  45. package/src/generators/slice/built-in-plan-infrastructure-files.js +304 -144
  46. package/src/generators/slice/built-in-plan-key-value-retention-infrastructure.js +2 -2
  47. package/src/generators/slice/built-in-plan-metadata-files.js +33 -1
  48. package/src/generators/slice/built-in-plan-operations-files.js +106 -6
  49. package/src/generators/slice/built-in-plan-project-files.js +1 -0
  50. package/src/generators/slice/built-in-plan-release-files.js +2 -43
  51. package/src/generators/slice/built-in-plan-reliability-files.js +222 -236
  52. package/src/generators/slice/built-in-plan-shared.d.ts +29 -3
  53. package/src/generators/slice/built-in-plan-shared.js +29 -2
  54. package/src/generators/slice/built-in-plan-web-files.js +18 -1
  55. package/src/generators/slice/built-in-plan.js +1 -0
  56. package/src/generators/slice/slice.js +2 -0
  57. package/src/generators/upgrade/upgrade.d.ts +4 -3
  58. package/src/generators/upgrade/upgrade.js +50 -26
  59. package/src/generators/workload/frozen-container.d.ts +1 -1
  60. package/src/generators/workload/service-performance-files.js +22 -5
  61. package/src/generators/workload/workload.js +5 -1
  62. package/src/local-dev/dev-command-core.d.ts +24 -3
  63. package/src/local-dev/dev-command-core.js +124 -12
  64. package/src/local-dev/errors.d.ts +19 -1
  65. package/src/local-dev/errors.js +18 -1
  66. package/src/local-dev/fleet-client.d.ts +64 -6
  67. package/src/local-dev/fleet-client.js +189 -15
  68. package/src/local-dev/fleet-process.d.ts +47 -2
  69. package/src/local-dev/fleet-process.js +248 -31
  70. package/src/local-dev/fleet-runtime.d.ts +37 -0
  71. package/src/local-dev/fleet-runtime.js +127 -69
  72. package/src/local-dev/fleet-session.d.ts +1 -0
  73. package/src/local-dev/fleet-session.js +42 -3
  74. package/src/local-dev/fleet-state.d.ts +5 -0
  75. package/src/local-dev/fleet-state.js +11 -2
  76. package/src/local-dev/fleet-supervisor.d.ts +6 -0
  77. package/src/local-dev/fleet-supervisor.js +49 -1
  78. package/src/local-dev/ports.d.ts +11 -1
  79. package/src/local-dev/ports.js +78 -38
  80. package/src/local-dev/slice-runner.d.ts +19 -1
  81. package/src/local-dev/slice-runner.js +290 -91
  82. package/src/repository-policy/deployment-plan.d.ts +1 -1
  83. package/src/repository-policy/deployment-plan.js +16 -1
  84. package/src/secrets/generated-aws-control-plane.d.ts +11 -1
  85. package/src/secrets/generated-aws-control-plane.js +92 -29
  86. package/src/secrets/operator.d.ts +3 -2
  87. package/src/secrets/operator.js +27 -5
  88. package/src/supply-chain/generated-files.js +9 -0
@@ -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;
@@ -122,7 +122,7 @@ function validateFleetSliceRunnerState(value) {
122
122
  'sessionGeneration',
123
123
  'slice',
124
124
  'updatedAt',
125
- ], ['child', 'failure'], 'Fleet Slice runner state');
125
+ ], ['child', 'childExit', 'failure'], 'Fleet Slice runner state');
126
126
  if (value.schemaVersion !== exports.FLEET_SLICE_RUNNER_SCHEMA_VERSION ||
127
127
  !['failed', 'pending', 'running', 'succeeded'].includes(String(value.cleanup))) {
128
128
  return runnerError('Fleet Slice runner state is invalid.');
@@ -140,11 +140,35 @@ function validateFleetSliceRunnerState(value) {
140
140
  if (value.child !== undefined) {
141
141
  state.child = processIdentity(value.child, 'Fleet Slice runner state child');
142
142
  }
143
+ if (value.childExit !== undefined) {
144
+ state.childExit = validateRunnerChildExit(value.childExit);
145
+ }
143
146
  if (value.failure !== undefined) {
144
147
  state.failure = (0, contract_validation_js_1.requireBoundedString)(value.failure, 'Fleet Slice runner state failure', { maximumLength: 1_024 });
145
148
  }
146
149
  return state;
147
150
  }
151
+ function validateRunnerChildExit(value) {
152
+ (0, contract_validation_js_1.assertRecord)(value, 'Fleet Slice runner child exit');
153
+ (0, contract_validation_js_1.assertExactProperties)(value, ['at', 'code', 'signal'], [], 'Fleet Slice runner child exit');
154
+ const code = value.code === null
155
+ ? null
156
+ : (0, contract_validation_js_1.requireInteger)(value.code, 'Fleet Slice runner child exit code', -2_147_483_648, 4_294_967_295);
157
+ const signal = value.signal === null
158
+ ? null
159
+ : (0, contract_validation_js_1.requireBoundedString)(value.signal, 'Fleet Slice runner child signal', {
160
+ maximumLength: 32,
161
+ pattern: /^SIG[A-Z0-9]+$/,
162
+ });
163
+ if ((code === null) === (signal === null)) {
164
+ return runnerError('Fleet Slice runner child exit must carry exactly one of status or signal.');
165
+ }
166
+ return {
167
+ at: (0, contract_validation_js_1.requireIsoTimestamp)(value.at, 'Fleet Slice runner child exit at'),
168
+ code,
169
+ signal,
170
+ };
171
+ }
148
172
  function runnerStem(slice, runnerGeneration) {
149
173
  return `${(0, fleet_lock_js_1.validateFleetIdentitySlug)(slice, 'Fleet Slice runner Slice')}-${(0, contract_validation_js_1.sha256Digest)({ runnerGeneration: generation(runnerGeneration, 'runner generation') }).slice(0, 20)}`;
150
174
  }
@@ -184,8 +208,17 @@ function validateFleetSliceRunnerRestartRequest(value) {
184
208
  slice: (0, fleet_lock_js_1.validateFleetIdentitySlug)(value.slice, 'Fleet Slice runner restart Slice'),
185
209
  };
186
210
  }
211
+ /** A runner publishes its state once its Nx plan is loaded, or earlier when
212
+ * quiescence reaches it during the load. Name that condition instead of
213
+ * surfacing the private-file check's missing-file error. */
214
+ function readPublishedFleetSliceRunnerState(statePath, security, consequence) {
215
+ if (!(0, node_fs_1.existsSync)(statePath)) {
216
+ throw new errors_js_1.LocalDevContractError('cleanup-incomplete', `Fleet Slice runner has not published its state; it is still loading its Nx plan or exited before starting. ${consequence}`);
217
+ }
218
+ return readFleetSliceRunnerState(statePath, security);
219
+ }
187
220
  function requestFleetSliceRunnerRestart(launched, effects = {}) {
188
- const state = readFleetSliceRunnerState(launched.statePath, effects.security ?? {});
221
+ const state = readPublishedFleetSliceRunnerState(launched.statePath, effects.security ?? {}, 'Restart is refused until the Slice reports starting.');
189
222
  if (state.fleetId !== launched.request.fleetId ||
190
223
  state.runnerGeneration !== launched.request.runnerGeneration ||
191
224
  state.sessionGeneration !== launched.request.sessionGeneration ||
@@ -372,7 +405,8 @@ function runnerParentStatus(request, options = {}) {
372
405
  ? 'lost'
373
406
  : 'ambiguous';
374
407
  }
375
- async function runFleetSliceRunnerCore(requestPath, options = {}, stopSignal) {
408
+ async function runFleetSliceRunnerCore(requestPath, options = {}, control) {
409
+ const { stopSignal } = control;
376
410
  const security = options.security ?? {};
377
411
  const now = options.now ?? (() => new Date());
378
412
  const request = readFleetSliceRunnerRequest(requestPath, security);
@@ -382,6 +416,47 @@ async function runFleetSliceRunnerCore(requestPath, options = {}, stopSignal) {
382
416
  startup.workspaceRealpath !== request.workspaceRealpath) {
383
417
  return runnerError('Fleet Slice runner request does not match its startup authority.');
384
418
  }
419
+ const paths = fleetSliceRunnerPaths((0, node_path_1.join)((0, node_path_1.dirname)(request.startupPath), 'runners'), request.slice, request.runnerGeneration);
420
+ // A runner launched over retained state is a cleanup retry and never
421
+ // starts a writer. Decide that before quiescence can publish fresh state.
422
+ const retry = (0, node_fs_1.existsSync)(paths.state);
423
+ const runner = (options.processIdentity ?? process_identity_js_1.captureProcessIdentity)();
424
+ const pendingState = () => ({
425
+ cleanup: 'pending',
426
+ fleetId: request.fleetId,
427
+ runner,
428
+ runnerGeneration: request.runnerGeneration,
429
+ schemaVersion: exports.FLEET_SLICE_RUNNER_SCHEMA_VERSION,
430
+ sessionGeneration: request.sessionGeneration,
431
+ slice: request.slice,
432
+ updatedAt: now().toISOString(),
433
+ });
434
+ let phase = 'loading';
435
+ let published;
436
+ let quiesceFailure;
437
+ let quiesceRunning = () => undefined;
438
+ if (!retry) {
439
+ control.onQuiesce(() => {
440
+ if (phase === 'loading') {
441
+ // Quiescence reached this runner while it loads its Nx plan. No
442
+ // writer has started and none will: publish pending cleanup
443
+ // authority first, so a later stop runs the declared cleanup here
444
+ // and a restart intent can bind to this exact generation.
445
+ try {
446
+ published = writeFleetSliceRunnerState(paths.state, pendingState(), security);
447
+ phase = 'quiesced';
448
+ control.acknowledgeQuiesce();
449
+ }
450
+ catch (error) {
451
+ quiesceFailure = error;
452
+ }
453
+ }
454
+ else if (phase === 'running')
455
+ quiesceRunning();
456
+ else if (phase === 'quiesced')
457
+ control.acknowledgeQuiesce();
458
+ });
459
+ }
385
460
  const plan = await (options.loadPlan ?? nx_workspace_js_1.loadWorkspaceFleetPlan)();
386
461
  const adapter = plan.adapters.get(request.slice);
387
462
  if (!adapter) {
@@ -390,8 +465,6 @@ async function runFleetSliceRunnerCore(requestPath, options = {}, stopSignal) {
390
465
  const startTarget = (0, nx_workspace_js_1.declaredStartTarget)(plan, request.slice);
391
466
  const cleanupTarget = (0, nx_workspace_js_1.declaredCleanupTarget)(plan, request.slice);
392
467
  const runtime = (options.resolveRuntime ?? nx_workspace_js_1.resolveWorkspaceNxRuntime)(request.workspaceRealpath);
393
- const paths = fleetSliceRunnerPaths((0, node_path_1.join)((0, node_path_1.dirname)(request.startupPath), 'runners'), request.slice, request.runnerGeneration);
394
- const runner = (options.processIdentity ?? process_identity_js_1.captureProcessIdentity)();
395
468
  const origin = new URL(request.origin);
396
469
  const childEnvironment = (0, personal_stage_js_1.createPersonalDevelopmentEnvironment)(startup.identity.stage, {
397
470
  ...process.env,
@@ -417,9 +490,9 @@ async function runFleetSliceRunnerCore(requestPath, options = {}, stopSignal) {
417
490
  };
418
491
  const terminate = options.terminate ??
419
492
  ((child) => (0, fleet_process_js_1.terminateOwnedChild)(child, ownedChildren.get(child)));
420
- if (stopSignal.aborted)
421
- throw new Error('Fleet Slice runner stopped before child execution.');
422
- if ((0, node_fs_1.existsSync)(paths.state)) {
493
+ if (retry) {
494
+ if (stopSignal.aborted)
495
+ throw new Error('Fleet Slice runner stopped before child execution.');
423
496
  let previous = readFleetSliceRunnerState(paths.state, security);
424
497
  if (previous.fleetId !== request.fleetId ||
425
498
  previous.runnerGeneration !== request.runnerGeneration ||
@@ -488,57 +561,92 @@ async function runFleetSliceRunnerCore(requestPath, options = {}, stopSignal) {
488
561
  return 1;
489
562
  }
490
563
  }
491
- let state = writeFleetSliceRunnerState(paths.state, {
492
- cleanup: 'pending',
493
- fleetId: request.fleetId,
494
- runner,
495
- runnerGeneration: request.runnerGeneration,
496
- schemaVersion: exports.FLEET_SLICE_RUNNER_SCHEMA_VERSION,
497
- sessionGeneration: request.sessionGeneration,
498
- slice: request.slice,
499
- updatedAt: now().toISOString(),
500
- }, security);
501
- const child = spawnTarget(runtime, startTarget, {
502
- additionalEnvironment: childEnvironment,
503
- port: request.port,
504
- stage: startup.identity.stage,
505
- stdio: 'inherit',
506
- workspaceRoot: request.workspaceRealpath,
507
- });
508
- try {
509
- if (!child.pid)
510
- throw new Error('Slice child did not expose a PID.');
511
- state = writeFleetSliceRunnerState(paths.state, {
512
- ...state,
513
- child: identifyChild(child),
514
- updatedAt: now().toISOString(),
515
- }, security);
564
+ if (quiesceFailure !== undefined) {
565
+ // Quiescence was requested but its pending authority could not be
566
+ // published: refuse to start a writer and report the failed runner.
567
+ throw quiesceFailure instanceof Error
568
+ ? quiesceFailure
569
+ : new Error(String(quiesceFailure));
516
570
  }
517
- catch (error) {
518
- await terminate(child).catch(() => undefined);
571
+ let child;
572
+ let state;
573
+ if (phase === 'quiesced' || stopSignal.aborted) {
574
+ // Quiescence or a stop reached this runner while it loaded its plan, so
575
+ // its writer never starts. The stop handled below then runs declared
576
+ // cleanup here, or a restart intent leaves cleanup pending.
577
+ state =
578
+ published ??
579
+ writeFleetSliceRunnerState(paths.state, pendingState(), security);
580
+ phase = 'quiesced';
581
+ }
582
+ else {
583
+ state = writeFleetSliceRunnerState(paths.state, pendingState(), security);
584
+ const started = spawnTarget(runtime, startTarget, {
585
+ additionalEnvironment: childEnvironment,
586
+ port: request.port,
587
+ stage: startup.identity.stage,
588
+ stdio: 'inherit',
589
+ workspaceRoot: request.workspaceRealpath,
590
+ });
591
+ try {
592
+ if (!started.pid)
593
+ throw new Error('Slice child did not expose a PID.');
594
+ state = writeFleetSliceRunnerState(paths.state, {
595
+ ...state,
596
+ child: identifyChild(started),
597
+ updatedAt: now().toISOString(),
598
+ }, security);
599
+ }
600
+ catch (error) {
601
+ await terminate(started).catch(() => undefined);
602
+ state = writeFleetSliceRunnerState(paths.state, {
603
+ ...state,
604
+ cleanup: 'failed',
605
+ failure: error instanceof Error ? error.message : String(error),
606
+ updatedAt: now().toISOString(),
607
+ }, security);
608
+ return 1;
609
+ }
610
+ child = started;
611
+ phase = 'running';
612
+ }
613
+ let finalizing;
614
+ let quiescing;
615
+ let interval;
616
+ let settle = () => undefined;
617
+ let abandon = () => undefined;
618
+ const done = new Promise((resolvePromise, reject) => {
619
+ settle = resolvePromise;
620
+ abandon = reject;
621
+ });
622
+ const recordFailure = (error) => {
519
623
  state = writeFleetSliceRunnerState(paths.state, {
520
624
  ...state,
521
625
  cleanup: 'failed',
522
626
  failure: error instanceof Error ? error.message : String(error),
523
627
  updatedAt: now().toISOString(),
524
628
  }, security);
525
- return 1;
526
- }
527
- let finalizing;
528
- let interval;
629
+ };
630
+ // The writer is gone only when its whole owned group is absent.
631
+ const settleChild = async (terminateChild) => {
632
+ if (!child)
633
+ return;
634
+ if (terminateChild)
635
+ await terminate(child);
636
+ if (!child.pid)
637
+ throw new Error('Slice child PID is unavailable.');
638
+ await (0, fleet_process_js_1.assertOwnedGroupAbsent)(child.pid);
639
+ };
529
640
  const finalize = (terminateChild, cleanup) => {
530
641
  if (finalizing)
531
642
  return finalizing;
643
+ phase = 'finalizing';
644
+ if (interval)
645
+ clearInterval(interval);
532
646
  finalizing = (async () => {
533
- if (interval)
534
- clearInterval(interval);
535
647
  if (!cleanup) {
536
648
  try {
537
- if (terminateChild)
538
- await terminate(child);
539
- if (!child.pid)
540
- throw new Error('Slice child PID is unavailable.');
541
- await (0, fleet_process_js_1.assertOwnedGroupAbsent)(child.pid);
649
+ await settleChild(terminateChild);
542
650
  state = writeFleetSliceRunnerState(paths.state, {
543
651
  ...state,
544
652
  cleanup: 'pending',
@@ -547,22 +655,15 @@ async function runFleetSliceRunnerCore(requestPath, options = {}, stopSignal) {
547
655
  return 0;
548
656
  }
549
657
  catch (error) {
550
- state = writeFleetSliceRunnerState(paths.state, {
551
- ...state,
552
- cleanup: 'failed',
553
- failure: error instanceof Error ? error.message : String(error),
554
- updatedAt: now().toISOString(),
555
- }, security);
658
+ recordFailure(error);
556
659
  return 1;
557
660
  }
558
661
  }
559
662
  state = writeFleetSliceRunnerState(paths.state, { ...state, cleanup: 'running', updatedAt: now().toISOString() }, security);
560
663
  try {
561
- if (terminateChild)
562
- await terminate(child);
563
- if (!child.pid)
564
- throw new Error('Slice child PID is unavailable.');
565
- await (0, fleet_process_js_1.assertOwnedGroupAbsent)(child.pid);
664
+ await settleChild(terminateChild);
665
+ // The cleanup child inherits this runner's stdio, which the Fleet
666
+ // supervisor captures into the Slice log store.
566
667
  await runDeclaredCleanupTarget(cleanupTarget, runtime, childEnvironment, startup.identity.stage, request.workspaceRealpath, spawnTarget, (cleanup) => {
567
668
  if (!cleanup.pid)
568
669
  throw new Error('Cleanup child did not expose a PID.');
@@ -585,31 +686,94 @@ async function runFleetSliceRunnerCore(requestPath, options = {}, stopSignal) {
585
686
  return 0;
586
687
  }
587
688
  catch (error) {
588
- state = writeFleetSliceRunnerState(paths.state, {
589
- ...state,
590
- cleanup: 'failed',
591
- failure: error instanceof Error ? error.message : String(error),
592
- updatedAt: now().toISOString(),
593
- }, security);
689
+ recordFailure(error);
594
690
  return 1;
595
691
  }
596
692
  })();
693
+ void finalizing.then(settle);
597
694
  return finalizing;
598
695
  };
599
- const terminateSignal = () => {
696
+ // Every exit request funnels here. A quiescence in progress finishes
697
+ // first, so finalization never races the writer it is still stopping.
698
+ const finish = (cleanup) => {
699
+ if (phase === 'finalizing')
700
+ return;
701
+ if (phase === 'quiescing') {
702
+ void quiescing?.then(() => finish(cleanup), () => undefined);
703
+ return;
704
+ }
705
+ void finalize(phase === 'running', cleanup);
706
+ };
707
+ quiesceRunning = () => {
708
+ if (phase !== 'running')
709
+ return;
710
+ phase = 'quiescing';
711
+ // Stop the writer but keep this runner and its pending cleanup
712
+ // authority: peer writers may still be live, so cleanup waits for an
713
+ // explicit stop after every selected Slice has quiesced.
714
+ quiescing = (async () => {
715
+ await settleChild(true);
716
+ state = writeFleetSliceRunnerState(paths.state, { ...state, cleanup: 'pending', updatedAt: now().toISOString() }, security);
717
+ })();
718
+ quiescing.then(() => {
719
+ if (phase !== 'quiescing')
720
+ return;
721
+ phase = 'quiesced';
722
+ control.acknowledgeQuiesce();
723
+ }, (error) => {
724
+ if (phase !== 'quiescing')
725
+ return;
726
+ phase = 'finalizing';
727
+ if (interval)
728
+ clearInterval(interval);
729
+ try {
730
+ recordFailure(error);
731
+ }
732
+ catch {
733
+ // The failed exit status below still reports the refusal.
734
+ }
735
+ finalizing = Promise.resolve(1);
736
+ settle(1);
737
+ });
738
+ };
739
+ const onStop = () => {
600
740
  const reason = consumeFleetSliceRunnerRestart(paths.restart, request, security)
601
741
  ? 'explicit-restart'
602
742
  : 'termination';
603
- void finalize(true, fleetSliceRunnerRequiresCleanup(reason));
743
+ finish(fleetSliceRunnerRequiresCleanup(reason));
604
744
  };
605
- stopSignal.addEventListener('abort', terminateSignal, { once: true });
606
- if (stopSignal.aborted)
607
- terminateSignal();
608
- // A child exit is restartable process failure, not consent to destroy local
609
- // prerequisites. The exact runner request and pending cleanup evidence stay
610
- // behind so a later explicit down or proved-stale recovery can clean them.
611
- const childFinished = childExit(child).then(() => finalize(false, fleetSliceRunnerRequiresCleanup('child-exit')));
612
- const parentLost = new Promise((resolvePromise) => {
745
+ if (child) {
746
+ const started = child;
747
+ started.once('error', abandon);
748
+ // A child exit is restartable process failure, not consent to destroy
749
+ // local prerequisites. The exact runner request and pending cleanup
750
+ // evidence stay behind so a later explicit down or proved-stale recovery
751
+ // can clean them.
752
+ started.once('exit', (code, signal) => {
753
+ // Owned termination (quiescence, restart, stop, lost parent) changes
754
+ // the phase first; only an exit the runner did not cause is the
755
+ // Slice's own failure cause.
756
+ if (phase !== 'running')
757
+ return;
758
+ if (code !== null || signal !== null) {
759
+ try {
760
+ state = writeFleetSliceRunnerState(paths.state, {
761
+ ...state,
762
+ childExit: { at: now().toISOString(), code, signal },
763
+ updatedAt: now().toISOString(),
764
+ }, security);
765
+ }
766
+ catch {
767
+ // The diagnostic is best effort; finalization below owns cleanup.
768
+ }
769
+ }
770
+ void finalize(false, fleetSliceRunnerRequiresCleanup('child-exit'));
771
+ });
772
+ }
773
+ stopSignal.addEventListener('abort', onStop, { once: true });
774
+ try {
775
+ // Referenced: a quiesced runner without a child must stay alive to
776
+ // observe its parent until an explicit stop arrives.
613
777
  interval = setInterval(() => {
614
778
  if (runnerParentStatus(request, {
615
779
  graceMs: options.parentGraceMs,
@@ -617,35 +781,63 @@ async function runFleetSliceRunnerCore(requestPath, options = {}, stopSignal) {
617
781
  processProbe: options.processProbe?.(request.parent),
618
782
  security,
619
783
  }) === 'lost') {
620
- void finalize(true, fleetSliceRunnerRequiresCleanup('parent-lost')).then(resolvePromise);
784
+ finish(fleetSliceRunnerRequiresCleanup('parent-lost'));
621
785
  }
622
786
  }, options.monitorIntervalMs ?? 1_000);
623
- interval.unref();
624
- });
625
- const result = await Promise.race([childFinished, parentLost]);
626
- stopSignal.removeEventListener('abort', terminateSignal);
627
- return (result ??
628
- (await finalize(true, fleetSliceRunnerRequiresCleanup('termination'))));
787
+ if (stopSignal.aborted)
788
+ onStop();
789
+ return await done;
790
+ }
791
+ finally {
792
+ stopSignal.removeEventListener('abort', onStop);
793
+ if (interval)
794
+ clearInterval(interval);
795
+ }
629
796
  }
630
797
  async function runFleetSliceRunner(requestPath, options = {}) {
631
798
  const request = readFleetSliceRunnerRequest(requestPath, options.security);
632
799
  const stop = new AbortController();
800
+ let quiesce;
801
+ let quiesceRequested = false;
633
802
  const interrupt = () => stop.abort();
803
+ const control = (value, type) => Boolean(value &&
804
+ typeof value === 'object' &&
805
+ Object.keys(value).sort().join(',') === 'runnerGeneration,type' &&
806
+ 'type' in value &&
807
+ value.type === type &&
808
+ 'runnerGeneration' in value &&
809
+ value.runnerGeneration === request.runnerGeneration);
634
810
  const message = (value) => {
635
- if (value &&
636
- typeof value === 'object' &&
637
- Object.keys(value).sort().join(',') === 'runnerGeneration,type' &&
638
- 'type' in value &&
639
- value.type === fleet_process_js_1.FLEET_RUNNER_STOP_MESSAGE &&
640
- 'runnerGeneration' in value &&
641
- value.runnerGeneration === request.runnerGeneration)
811
+ if (control(value, fleet_process_js_1.FLEET_RUNNER_STOP_MESSAGE))
642
812
  interrupt();
813
+ else if (control(value, fleet_process_js_1.FLEET_RUNNER_QUIESCE_MESSAGE)) {
814
+ if (quiesce)
815
+ quiesce();
816
+ else
817
+ quiesceRequested = true;
818
+ }
643
819
  };
644
820
  process.once('SIGINT', interrupt);
645
821
  process.once('SIGTERM', interrupt);
646
822
  process.on('message', message);
647
823
  try {
648
- return await runFleetSliceRunnerCore(requestPath, options, stop.signal);
824
+ return await runFleetSliceRunnerCore(requestPath, options, {
825
+ acknowledgeQuiesce: () => {
826
+ if (process.connected)
827
+ process.send?.({
828
+ runnerGeneration: request.runnerGeneration,
829
+ type: fleet_process_js_1.FLEET_RUNNER_QUIESCED_MESSAGE,
830
+ }, () => undefined);
831
+ },
832
+ onQuiesce: (handler) => {
833
+ quiesce = handler;
834
+ if (!quiesceRequested)
835
+ return;
836
+ quiesceRequested = false;
837
+ handler();
838
+ },
839
+ stopSignal: stop.signal,
840
+ });
649
841
  }
650
842
  finally {
651
843
  process.removeListener('SIGINT', interrupt);
@@ -654,7 +846,7 @@ async function runFleetSliceRunner(requestPath, options = {}) {
654
846
  }
655
847
  }
656
848
  function assertFleetSliceRunnerCleanup(statePath, expected, security = {}) {
657
- const state = readFleetSliceRunnerState(statePath, security);
849
+ const state = readPublishedFleetSliceRunnerState(statePath, security, 'Its claim remains retained.');
658
850
  if (state.cleanup !== 'succeeded' ||
659
851
  state.fleetId !== expected.fleetId ||
660
852
  state.runnerGeneration !== expected.runnerGeneration ||
@@ -667,7 +859,7 @@ async function retryFleetSliceRunnerCleanup(launched, effects = {}) {
667
859
  const security = effects.security ?? {};
668
860
  let state;
669
861
  try {
670
- state = readFleetSliceRunnerState(launched.statePath, security);
862
+ state = readPublishedFleetSliceRunnerState(launched.statePath, security, 'Its claim remains retained.');
671
863
  }
672
864
  catch (error) {
673
865
  throw new errors_js_1.LocalDevContractError('cleanup-incomplete', `Fleet Slice cleanup retry lacks validated runner state: ${error instanceof Error ? error.message : String(error)}`);
@@ -713,9 +905,16 @@ async function retryFleetSliceRunnerCleanup(launched, effects = {}) {
713
905
  EBK_FLEET_SLICE_RUNNER_REQUEST: launched.requestPath,
714
906
  }),
715
907
  shell: false,
716
- stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
908
+ stdio: effects.output
909
+ ? ['ignore', 'pipe', 'pipe', 'ipc']
910
+ : ['ignore', 'inherit', 'inherit', 'ipc'],
717
911
  windowsHide: true,
718
912
  });
913
+ const output = effects.output;
914
+ if (output) {
915
+ child.stdout?.on('data', (chunk) => output('stdout', chunk));
916
+ child.stderr?.on('data', (chunk) => output('stderr', chunk));
917
+ }
719
918
  // Client observation deadlines do not cancel a native cleanup operation.
720
919
  // Explicit signals and generation-bound IPC still stop the coordinator;
721
920
  // only its terminal exit and validated receipt can complete this retry.
@@ -1,6 +1,6 @@
1
1
  import type { ConfiguredDeploymentAccounts, DeploymentProfile, ProductionApprovalMode, ProductionFoundationProfile, RepositoryOperatingPolicy } from './policy.js';
2
2
  export declare const PROTECTED_DEPLOYMENT_ACTIONS: readonly ["deploy", "refresh", "remove", "unlock"];
3
- export declare const PROTECTED_DEPLOYMENT_STAGES: readonly ["staging", "production"];
3
+ export declare const PROTECTED_DEPLOYMENT_STAGES: readonly ["development", "staging", "production"];
4
4
  export type ProtectedDeploymentAction = (typeof PROTECTED_DEPLOYMENT_ACTIONS)[number];
5
5
  export type ProtectedDeploymentStage = (typeof PROTECTED_DEPLOYMENT_STAGES)[number];
6
6
  export interface DeploymentPlanProject {
@@ -9,7 +9,11 @@ exports.PROTECTED_DEPLOYMENT_ACTIONS = [
9
9
  'remove',
10
10
  'unlock',
11
11
  ];
12
- exports.PROTECTED_DEPLOYMENT_STAGES = ['staging', 'production'];
12
+ exports.PROTECTED_DEPLOYMENT_STAGES = [
13
+ 'development',
14
+ 'staging',
15
+ 'production',
16
+ ];
13
17
  function supportsAction(project, action) {
14
18
  return Object.prototype.hasOwnProperty.call(project.targets ?? {}, action);
15
19
  }
@@ -20,6 +24,17 @@ function compareCodePoints(left, right) {
20
24
  return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
21
25
  }
22
26
  function createProtectedDeploymentPlan(policy, options) {
27
+ // Only the starter protected pair deploys development through the protected
28
+ // workflow, from protected develop. Isolated profiles rehearse staging there,
29
+ // and the trunk lane protects main alone.
30
+ if (options.stage === 'development') {
31
+ if (policy.deploymentProfile !== 'starter') {
32
+ throw new Error(`Protected development deployment is unavailable for the ${policy.deploymentProfile} deployment profile; its protected develop lifecycle deploys staging.`);
33
+ }
34
+ if ((0, policy_js_1.deliveryLaneOf)(policy) === 'trunk') {
35
+ throw new Error('Protected development deployment is unavailable in the trunk delivery lane; development deploys locally and the protected lifecycle runs from main.');
36
+ }
37
+ }
23
38
  (0, policy_js_1.configuredDeploymentAccountForStage)(policy.deploymentProfile, policy.deploymentAccounts, options.stage);
24
39
  const deploymentAccounts = { ...policy.deploymentAccounts };
25
40
  const deploymentRegion = (0, policy_js_1.configuredDeploymentRegion)(policy.deploymentRegion);
@@ -4,9 +4,18 @@ export type AwsGeneratedSecretCommand = CreateSecretCommand | DescribeSecretComm
4
4
  export interface AwsGeneratedSecretClient {
5
5
  send(command: AwsGeneratedSecretCommand): Promise<unknown>;
6
6
  }
7
+ declare const observationPhases: readonly ["inspect", "pre-create", "post-create"];
8
+ declare const observationPredicates: readonly ["response-record", "arn-type", "arn-name", "name", "description", "deleted-date", "rotation-enabled", "tags-shape", "tags-count", "tag-entry", "tag-duplicate", "tag-value", "version-state", "not-found"];
9
+ export interface GeneratedSecretObservationDiagnostic {
10
+ readonly phase: (typeof observationPhases)[number];
11
+ readonly predicate: (typeof observationPredicates)[number];
12
+ }
13
+ /** Copy only static labels; never invoke getters or retain provider detail. */
14
+ export declare function safeGeneratedSecretObservationDiagnostic(value: unknown): GeneratedSecretObservationDiagnostic | undefined;
7
15
  export declare class AwsGeneratedSecretInitializationError extends Error {
8
16
  readonly code: 'generated-secret-input-invalid' | 'generated-secret-observation-invalid' | 'generated-secret-read-failed' | 'generated-secret-state-conflicting' | 'generated-secret-write-failed';
9
- constructor(code: AwsGeneratedSecretInitializationError['code']);
17
+ readonly observationDiagnostic?: GeneratedSecretObservationDiagnostic;
18
+ constructor(code: AwsGeneratedSecretInitializationError['code'], observationDiagnostic?: GeneratedSecretObservationDiagnostic);
10
19
  }
11
20
  /**
12
21
  * Generates one declared machine value inside the AWS adapter and atomically
@@ -21,3 +30,4 @@ export declare function createAwsGeneratedSecretInitializationControlPlane(optio
21
30
  randomBytes?: (size: number) => Buffer;
22
31
  wait?: (milliseconds: number) => Promise<void>;
23
32
  }): GeneratedSecretInitializationControlPlane;
33
+ export {};