@rehearsal-db/core 1.2.0 → 1.3.0

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 (41) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/COMPATIBILITY.md +15 -6
  3. package/README.md +1 -1
  4. package/dist/package.json +2 -1
  5. package/dist/src/cli/arguments.d.mts +1 -0
  6. package/dist/src/cli/arguments.mjs +4 -0
  7. package/dist/src/cli/basic_command_handlers.mjs +2 -0
  8. package/dist/src/cli/cli_commands.mjs +2 -0
  9. package/dist/src/cli/project_commands.d.mts +8 -0
  10. package/dist/src/cli/runtime_commands.d.mts +2 -19
  11. package/dist/src/cli/runtime_commands.mjs +3 -90
  12. package/dist/src/cli/runtime_stack_runner.d.mts +41 -0
  13. package/dist/src/cli/runtime_stack_runner.mjs +104 -0
  14. package/dist/src/cli/runtime_workload_admission.d.mts +8 -0
  15. package/dist/src/cli/runtime_workload_admission.mjs +22 -0
  16. package/dist/src/cli/source_commands.d.mts +8 -0
  17. package/dist/src/cli/workload_command_handlers.d.mts +2 -0
  18. package/dist/src/cli/workload_command_handlers.mjs +38 -0
  19. package/dist/src/project/configuration_contract.d.mts +5 -0
  20. package/dist/src/project/configuration_normalization.d.mts +4 -0
  21. package/dist/src/project/configuration_resolution.d.mts +4 -0
  22. package/dist/src/project/configuration_resource_policy.d.mts +4 -0
  23. package/dist/src/project/configuration_resource_policy.mjs +20 -1
  24. package/dist/src/project/configuration_schema.mjs +1 -0
  25. package/dist/src/runtime/workload_queue.d.mts +39 -0
  26. package/dist/src/runtime/workload_queue.mjs +211 -0
  27. package/dist/src/runtime/workload_queue_location.d.mts +1 -0
  28. package/dist/src/runtime/workload_queue_location.mjs +43 -0
  29. package/dist/src/runtime/workload_queue_store.d.mts +27 -0
  30. package/dist/src/runtime/workload_queue_store.mjs +182 -0
  31. package/dist/src/shared/cancellation.mjs +1 -0
  32. package/docs/architecture.md +9 -0
  33. package/docs/commands.md +26 -6
  34. package/docs/configuration.md +33 -1
  35. package/docs/getting-started.md +1 -1
  36. package/docs/releasing.md +4 -0
  37. package/docs/roadmap.md +8 -6
  38. package/docs/stability.md +4 -4
  39. package/docs/updating.md +2 -2
  40. package/docs/vscode.md +1 -1
  41. package/package.json +2 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,20 @@
3
3
  All notable Rehearsal package changes are documented here. The format follows Keep
4
4
  a Changelog, and published versions follow Semantic Versioning.
5
5
 
6
+ ## [Unreleased]
7
+
8
+ ## [1.3.0] - 2026-10-24
9
+
10
+ ### Added
11
+
12
+ - Optional free local-user workload admission for participating `start`, `reset`,
13
+ `run` and `migrate` lifecycle phases. Bounded waits, cancellation and explicit
14
+ dead-holder metadata recovery do not interrupt another project or expire live
15
+ owners. Read-only `workloads` reports participation, not available host/VM memory.
16
+ Existing configurations and editor field metadata remain unchanged when omitted.
17
+ Application proofs, interactive sessions and nonparticipating tools are outside
18
+ this first lifecycle-only queue; this is not a distributed or hard-memory scheduler.
19
+
6
20
  ## [1.2.0] - 2026-10-24
7
21
 
8
22
  ### Added
@@ -866,7 +880,9 @@ not unattended cleanup, and container age is not proof of user inactivity.
866
880
  publication uses short-lived trusted OIDC, and every release tag must already exist on
867
881
  protected `main`.
868
882
 
869
- [Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.1.0...HEAD
883
+ [Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.3.0...HEAD
884
+ [1.3.0]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.2.0...v1.3.0
885
+ [1.2.0]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.1.0...v1.2.0
870
886
  [1.1.0]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.0.2...v1.1.0
871
887
  [1.0.2]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.0.1...v1.0.2
872
888
  [1.0.1]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.0.0...v1.0.1
package/COMPATIBILITY.md CHANGED
@@ -38,14 +38,14 @@ through the project's existing version manager or use the helper's explicit exec
38
38
  path; see [OS setup](installers/README.md). Helpers never install Node or switch a
39
39
  project's runtime automatically.
40
40
 
41
- The 1.2.0 source candidate adds optional capability
41
+ Core 1.2.0 introduced optional capability
42
42
  `rehearsalContracts.migrationInsights: 1` and `inspect insights --json`. It preserves
43
43
  existing migration-plan v1, baseline formats, digest meaning and failure diagnosis.
44
44
  Consumers must check the capability and exact plan/file bindings; absent or
45
- unavailable static analysis is not safety evidence. This candidate version and its
46
- setup target do not establish publication or completed editor/native acceptance.
45
+ unavailable static analysis is not safety evidence. Package publication does not
46
+ establish completed editor, accessibility or genuine-provider acceptance.
47
47
 
48
- Further source work adds optional `rehearsalContracts.migrationImpact: 1` with
48
+ Core 1.2.0 also introduced optional `rehearsalContracts.migrationImpact: 1` with
49
49
  `inspect impact` and `measure impact`. Consumers must explicitly select a configured
50
50
  local target and bind measurement consent to the exact whole-plan digest. Preview
51
51
  does not contact a database; measurement reads only the current verified sandbox,
@@ -62,13 +62,22 @@ changes in the selected disposable sandbox; this is not a nonblocking query prom
62
62
  Counts are decimal strings or `null` with an explicit unknown reason, never a safety
63
63
  verdict, sequential simulation or production prediction. Partial/unknown measurement
64
64
  uses `not_ready` with the existing not-ready exit `1`; normal rehearsal gates and existing JSON contracts
65
- are unchanged. This unpublished source capability does not claim a new editor UI,
66
- new database family or additional platform/native acceptance.
65
+ are unchanged. This capability does not claim a new database family or additional
66
+ platform/native acceptance.
67
67
  Older strict editor activity consumers can reject the new `measure impact` operation
68
68
  kind, including a retained completed receipt. Pair this capability with an editor
69
69
  consumer explicitly supporting that kind; the CLI does not silently rename the
70
70
  operation, relax unknown-kind validation or update an active editor installation.
71
71
 
72
+ Core 1.3.0 introduces optional `rehearsalContracts.workloadQueue: 1`: local
73
+ `workloads` inspection, exact-confirmed dead-holder metadata recovery and opt-in
74
+ `resources.workloadQueue` lifecycle admission. Omitted configuration preserves prior
75
+ behavior. Participating clients cooperate within the same OS user, boot and process
76
+ namespace; older clients and other tools do not participate. This does not measure
77
+ available memory, govern standing runtimes or promise a hard machine-capacity budget.
78
+ The existing editor field-metadata contract is unchanged. See
79
+ [resource review policies](docs/configuration.md#resource-review-policies).
80
+
72
81
  ## Operating systems and execution location
73
82
 
74
83
  Core 1.0.1's platform boundary distinguishes the computer running VS Code from the
package/README.md CHANGED
@@ -47,7 +47,7 @@ it through a reviewed privacy policy, and retire the access again. It is never p
47
47
  production deployment tool.
48
48
 
49
49
  The CLI is free and MIT-licensed, with stable releases on
50
- [npm](https://www.npmjs.com/package/@rehearsal-db/core). This guide targets **1.2.0**;
50
+ [npm](https://www.npmjs.com/package/@rehearsal-db/core). This guide targets **1.3.0**;
51
51
  install only a version actually published there. Prefer a visual workflow?
52
52
  [Install the free VS Code Starter preview](docs/vscode.md). Its included features are
53
53
  free indefinitely for personal or commercial development—no account, card, or trial.
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rehearsal-db/core",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "private": false,
5
5
  "description": "Catch PostgreSQL and Supabase migrations that pass on empty databases but fail on real data—before deploy.",
6
6
  "repository": {
@@ -26,6 +26,7 @@
26
26
  "projectDiscovery": 1,
27
27
  "resourceInventory": 1,
28
28
  "resourceManager": 1,
29
+ "workloadQueue": 1,
29
30
  "migrationReview": 1,
30
31
  "migrationInsights": 1,
31
32
  "migrationImpact": 1,
@@ -20,6 +20,7 @@ export interface RehearsalCliFlags {
20
20
  imagesOnly: boolean;
21
21
  cleanupConfirmation: string | undefined;
22
22
  stopConfirmation: string | undefined;
23
+ workloadConfirmation: string | undefined;
23
24
  proposalPath: string | undefined;
24
25
  sourceAccessConfirmation: string | undefined;
25
26
  sourceRetirementConfirmation: string | undefined;
@@ -21,6 +21,7 @@ export const parseArguments = (arguments_) => {
21
21
  imagesOnly: false,
22
22
  cleanupConfirmation: undefined,
23
23
  stopConfirmation: undefined,
24
+ workloadConfirmation: undefined,
24
25
  proposalPath: undefined,
25
26
  sourceAccessConfirmation: undefined,
26
27
  sourceRetirementConfirmation: undefined,
@@ -56,6 +57,8 @@ export const parseArguments = (arguments_) => {
56
57
  flags.cleanupConfirmation = argument.slice("--confirm-cleanup=".length);
57
58
  else if (argument.startsWith("--confirm-stop="))
58
59
  flags.stopConfirmation = argument.slice("--confirm-stop=".length);
60
+ else if (argument.startsWith("--confirm-workloads="))
61
+ flags.workloadConfirmation = argument.slice("--confirm-workloads=".length);
59
62
  else if (argument.startsWith("--proposal="))
60
63
  flags.proposalPath = argument.slice("--proposal=".length);
61
64
  else if (argument.startsWith("--confirm-source-access="))
@@ -114,6 +117,7 @@ export const resetGuidedFlags = (flags) => {
114
117
  imagesOnly: false,
115
118
  cleanupConfirmation: undefined,
116
119
  stopConfirmation: undefined,
120
+ workloadConfirmation: undefined,
117
121
  proposalPath: undefined,
118
122
  sourceAccessConfirmation: undefined,
119
123
  sourceRetirementConfirmation: undefined,
@@ -27,6 +27,8 @@ Commands:
27
27
  activity Report idle, busy, stale, or invalid operation state
28
28
  environments List owned runtimes and Docker capacity without starting it
29
29
  resources Browse individual images, volumes, and other local resources
30
+ workloads Inspect participating local lifecycle queue metadata
31
+ workloads recover --confirm-workloads= Recover reviewed dead-holder metadata only
30
32
  explain Show the immutable execution plan
31
33
  run --dry-run Alias the exact explain plan without mutations
32
34
  run --confirm-candidates= Execute reset, migration, and verification locally
@@ -5,6 +5,7 @@ import { createCommandRegistry } from "./command_registry.mjs";
5
5
  import { createIdentityCommandHandlers } from "./identity_command_handlers.mjs";
6
6
  import { createInspectionCommandHandlers } from "./inspection_command_handlers.mjs";
7
7
  import { createEditorCommandHandlers } from "./editor_command_handlers.mjs";
8
+ import { createWorkloadCommandHandlers } from "./workload_command_handlers.mjs";
8
9
  import { createProjectCommandHandlers } from "./project_command_handlers.mjs";
9
10
  import { createRuntimeActionHandlers } from "./runtime_action_handlers.mjs";
10
11
  import { createSourceCommandHandlers } from "./source_command_handlers.mjs";
@@ -23,6 +24,7 @@ export const createCliCommandRegistry = ({ projectRoot, projectCommands, runtime
23
24
  startRuntimeApplication: runtimeCommands.startRuntimeApplication,
24
25
  }),
25
26
  ...createInspectionCommandHandlers(),
27
+ ...createWorkloadCommandHandlers(),
26
28
  ...createEditorCommandHandlers(),
27
29
  ...createApplicationCommandHandlers({
28
30
  openRuntimeApplication: runtimeCommands.openRuntimeApplication,
@@ -80,6 +80,10 @@ export declare const createProjectCommands: ({ projectRoot, runManager, }: {
80
80
  retainBaselineGenerations: number;
81
81
  }>;
82
82
  resources: Readonly<{
83
+ workloadQueue?: {
84
+ enabled: boolean;
85
+ waitTimeoutSeconds: number;
86
+ };
83
87
  staleAfterSeconds: number | null;
84
88
  retainedEnvironmentCount: number;
85
89
  maximumBytes: number | null;
@@ -232,6 +236,10 @@ export declare const createProjectCommands: ({ projectRoot, runManager, }: {
232
236
  retainBaselineGenerations: number;
233
237
  }>;
234
238
  resources: Readonly<{
239
+ workloadQueue?: {
240
+ enabled: boolean;
241
+ waitTimeoutSeconds: number;
242
+ };
235
243
  staleAfterSeconds: number | null;
236
244
  retainedEnvironmentCount: number;
237
245
  maximumBytes: number | null;
@@ -3,28 +3,11 @@ import type { RehearsalConfigPathOptions } from "../project/configuration.mjs";
3
3
  export { buildRuntimeStackPlan, runRuntimeStackDoctor, topologyCandidateSummary, } from "./runtime_planning.mjs";
4
4
  import { loadRuntimeTopology } from "../runtime/topology.mjs";
5
5
  import type { InstalledPackageFingerprint } from "../shared/operation_guard.mjs";
6
- import type { RuntimeStatusSnapshot } from "../runtime/status.mjs";
6
+ import { type ManagerReceipt, type RuntimeAction } from "./runtime_stack_runner.mjs";
7
7
  import type { RehearsalCliFlags } from "./arguments.mjs";
8
8
  import type { RehearsalErrorCategory } from "../shared/diagnostics.mjs";
9
9
  import type { CliSessionState } from "./command_contract.mjs";
10
10
  type RuntimeTopology = Awaited<ReturnType<typeof loadRuntimeTopology>>;
11
- type RuntimeAction = "run" | "start" | "migrate" | "reset" | "status" | "stop" | "discard" | "verify";
12
- interface ManagerReceipt {
13
- readonly action: RuntimeAction;
14
- readonly target: string;
15
- readonly durationMs: number;
16
- readonly output: string;
17
- readonly runtimeStatus?: RuntimeStatusSnapshot;
18
- }
19
- interface RuntimeStackResult {
20
- readonly topology: RuntimeTopology;
21
- readonly runtime: ManagerReceipt | Readonly<{
22
- action: RuntimeAction;
23
- durationMs: number;
24
- output: string;
25
- targets: readonly ManagerReceipt[];
26
- }>;
27
- }
28
11
  export declare const classifyManagerFailure: ({ action, output, inferredCategory, }: {
29
12
  action: RuntimeAction;
30
13
  output: string;
@@ -165,5 +148,5 @@ export declare const createRuntimeCommands: ({ packageRoot, projectRoot, manager
165
148
  flags: RehearsalCliFlags;
166
149
  planOptions: RehearsalConfigPathOptions;
167
150
  session: CliSessionState;
168
- }) => Promise<RuntimeStackResult>;
151
+ }) => Promise<import("./runtime_stack_runner.mjs").RuntimeStackResult>;
169
152
  };
@@ -1,6 +1,5 @@
1
1
  /** Orchestrate isolated database runtimes and project-owned application proofs. */
2
2
  import { spawnSync } from "node:child_process";
3
- import { randomUUID } from "node:crypto";
4
3
  import { access } from "node:fs/promises";
5
4
  import { join } from "node:path";
6
5
  import { performance } from "node:perf_hooks";
@@ -18,15 +17,11 @@ import { createCleanProcessEnvironment } from "../shared/process_environment.mjs
18
17
  import { getOwnedOperationProgressId, trackOperationStage, } from "../shared/operation_progress.mjs";
19
18
  import { buildRuntimeManagerArguments } from "../targets/target.mjs";
20
19
  import { parseRuntimeStatusSnapshot, renderRuntimeStatusSnapshot, } from "../runtime/status.mjs";
21
- import { applyRuntimePersistence, shouldStopRuntime, } from "../runtime/persistence.mjs";
22
- import { executeRuntimeTargetStack } from "./runtime_stack_execution.mjs";
20
+ import { applyRuntimePersistence } from "../runtime/persistence.mjs";
21
+ import { createRuntimeStackRunner, } from "./runtime_stack_runner.mjs";
23
22
  import { summarizeRuntimeManagerFailure, runtimePortFailureGuidance, } from "./runtime_failure_summary.mjs";
24
23
  import { readManagerMigrationFailure } from "../runtime/migration_failure.mjs";
25
- import { releaseOwnedColimaWhenIdle } from "../runtime/container_engine_ownership.mjs";
26
- import { inspectRehearsalResources } from "../runtime/resource_inventory.mjs";
27
- import { assertLocalDockerContext } from "../runtime/local_docker_context.mjs";
28
24
  import { runtimeConfigurationSha256 } from "../runtime/configuration_binding.mjs";
29
- import { assertResourcePolicyAllowsStartup, evaluateResourcePolicy, } from "../runtime/resource_policy.mjs";
30
25
  import { formatDuration, isHumanTerminal, promptForConfirmation, terminalStyle, useStyledPrompts, } from "./terminal.mjs";
31
26
  const parseCommand = (source) => {
32
27
  const values = [];
@@ -340,89 +335,7 @@ export const createRuntimeCommands = ({ packageRoot, projectRoot, managerPath, g
340
335
  : "runtime-lifecycle",
341
336
  target: options.targetName ?? "primary",
342
337
  }, () => executeManager(options));
343
- const runRuntimeStack = async ({ command, flags, planOptions, session, }) => {
344
- const topology = session.runtimeTopology ??
345
- (await loadRuntimeTopology(planOptions));
346
- const reverse = ["stop", "discard"].includes(command);
347
- if (["start", "run", "reset"].includes(command) &&
348
- topology.primary.config.resources.maximumBytes !== null) {
349
- assertResourcePolicyAllowsStartup(evaluateResourcePolicy({
350
- inventory: await inspectRehearsalResources(planOptions),
351
- policy: topology.primary.config.resources,
352
- }));
353
- }
354
- const selectedTargets = selectRuntimeTargets(topology.targets, flags.target, command);
355
- const targets = reverse ? [...selectedTargets].reverse() : selectedTargets;
356
- const engine = command === "status"
357
- ? null
358
- : assertLocalDockerContext({ cwd: topology.projectRoot });
359
- const engineBinding = engine
360
- ? { dockerContext: engine.context, dockerIdentity: engine.identity }
361
- : {};
362
- const operationId = randomUUID();
363
- const results = await executeRuntimeTargetStack({
364
- targets,
365
- cleanupOnFailure: command === "run"
366
- ? shouldStopRuntime(topology.primary.config.lifecycle.run)
367
- : command === "start" &&
368
- shouldStopRuntime(topology.primary.config.lifecycle.open),
369
- execute: async (target) => {
370
- const confirmation = session.targetConfirmations?.get(target.configPath);
371
- return runManager({
372
- action: command,
373
- flags,
374
- configPath: target.configPath,
375
- ...(confirmation === undefined ? {} : { confirmation }),
376
- ...(topology.targets.length > 1 ? { targetName: target.name } : {}),
377
- operationId,
378
- ...engineBinding,
379
- configSha256: runtimeConfigurationSha256(target.config),
380
- });
381
- },
382
- stop: (target) => runManager({
383
- action: "stop",
384
- flags,
385
- configPath: target.configPath,
386
- targetName: target.name,
387
- operationId,
388
- ...engineBinding,
389
- configSha256: runtimeConfigurationSha256(target.config),
390
- }),
391
- });
392
- const release = ["stop", "discard"].includes(command)
393
- ? releaseOwnedColimaWhenIdle({
394
- projectRoot: topology.projectRoot,
395
- targets: topology.targets,
396
- })
397
- : null;
398
- const withRelease = (receipt) => release
399
- ? {
400
- ...receipt,
401
- output: [receipt.output, release.message]
402
- .filter(Boolean)
403
- .join("\n"),
404
- }
405
- : receipt;
406
- if (results.length === 1)
407
- return { topology, runtime: withRelease(results[0]) };
408
- return {
409
- topology,
410
- runtime: {
411
- action: command,
412
- durationMs: Math.round(results.reduce((total, result) => total + result.durationMs, 0) *
413
- 100) / 100,
414
- output: [
415
- results
416
- .map((result) => `[${result.target}]\n${result.output}`)
417
- .join("\n\n"),
418
- release?.message,
419
- ]
420
- .filter(Boolean)
421
- .join("\n\n"),
422
- targets: results,
423
- },
424
- };
425
- };
338
+ const runRuntimeStack = createRuntimeStackRunner(runManager);
426
339
  const runApplicationProof = async (planOptions, options = {}) => {
427
340
  const { config, projectRoot: configuredRoot } = await loadRehearsalConfig(planOptions);
428
341
  return trackOperationStage({ phase: "application-proof", target: options.targetName ?? "primary" }, () => runProjectCommand({
@@ -0,0 +1,41 @@
1
+ import type { RehearsalConfigPathOptions } from "../project/configuration.mjs";
2
+ import { loadRuntimeTopology } from "../runtime/topology.mjs";
3
+ import type { RuntimeStatusSnapshot } from "../runtime/status.mjs";
4
+ import type { RehearsalCliFlags } from "./arguments.mjs";
5
+ import type { CliSessionState } from "./command_contract.mjs";
6
+ type RuntimeTopology = Awaited<ReturnType<typeof loadRuntimeTopology>>;
7
+ export type RuntimeAction = "run" | "start" | "migrate" | "reset" | "status" | "stop" | "discard" | "verify";
8
+ export interface ManagerReceipt {
9
+ readonly action: RuntimeAction;
10
+ readonly target: string;
11
+ readonly durationMs: number;
12
+ readonly output: string;
13
+ readonly runtimeStatus?: RuntimeStatusSnapshot;
14
+ }
15
+ export interface RuntimeStackResult {
16
+ readonly topology: RuntimeTopology;
17
+ readonly runtime: ManagerReceipt | Readonly<{
18
+ action: RuntimeAction;
19
+ durationMs: number;
20
+ output: string;
21
+ targets: readonly ManagerReceipt[];
22
+ }>;
23
+ }
24
+ interface ManagerInvocation {
25
+ action: RuntimeAction;
26
+ flags: RehearsalCliFlags;
27
+ configPath?: string;
28
+ confirmation?: string;
29
+ targetName?: string;
30
+ operationId?: string;
31
+ dockerContext?: string;
32
+ dockerIdentity?: string;
33
+ configSha256?: string;
34
+ }
35
+ export declare const createRuntimeStackRunner: (runManager: (options: ManagerInvocation) => Promise<ManagerReceipt>) => ({ command, flags, planOptions, session, }: {
36
+ command: RuntimeAction;
37
+ flags: RehearsalCliFlags;
38
+ planOptions: RehearsalConfigPathOptions;
39
+ session: CliSessionState;
40
+ }) => Promise<RuntimeStackResult>;
41
+ export {};
@@ -0,0 +1,104 @@
1
+ /** Coordinate configured runtime stacks separately from application sessions/proofs. */
2
+ import { randomUUID } from "node:crypto";
3
+ import { loadRuntimeTopology, selectRuntimeTargets, } from "../runtime/topology.mjs";
4
+ import { shouldStopRuntime } from "../runtime/persistence.mjs";
5
+ import { releaseOwnedColimaWhenIdle } from "../runtime/container_engine_ownership.mjs";
6
+ import { inspectRehearsalResources } from "../runtime/resource_inventory.mjs";
7
+ import { assertLocalDockerContext } from "../runtime/local_docker_context.mjs";
8
+ import { runtimeConfigurationSha256 } from "../runtime/configuration_binding.mjs";
9
+ import { assertResourcePolicyAllowsStartup, evaluateResourcePolicy, } from "../runtime/resource_policy.mjs";
10
+ import { executeRuntimeTargetStack } from "./runtime_stack_execution.mjs";
11
+ import { withRuntimeWorkloadAdmission } from "./runtime_workload_admission.mjs";
12
+ export const createRuntimeStackRunner = (runManager) => async ({ command, flags, planOptions, session, }) => {
13
+ const topology = session.runtimeTopology ??
14
+ (await loadRuntimeTopology(planOptions));
15
+ const executeStack = async () => {
16
+ const reverse = ["stop", "discard"].includes(command);
17
+ if (["start", "run", "reset"].includes(command) &&
18
+ topology.primary.config.resources.maximumBytes !== null) {
19
+ assertResourcePolicyAllowsStartup(evaluateResourcePolicy({
20
+ inventory: await inspectRehearsalResources(planOptions),
21
+ policy: topology.primary.config.resources,
22
+ }));
23
+ }
24
+ const selectedTargets = selectRuntimeTargets(topology.targets, flags.target, command);
25
+ const targets = reverse
26
+ ? [...selectedTargets].reverse()
27
+ : selectedTargets;
28
+ const engine = command === "status"
29
+ ? null
30
+ : assertLocalDockerContext({ cwd: topology.projectRoot });
31
+ const engineBinding = engine
32
+ ? { dockerContext: engine.context, dockerIdentity: engine.identity }
33
+ : {};
34
+ const operationId = randomUUID();
35
+ const results = await executeRuntimeTargetStack({
36
+ targets,
37
+ cleanupOnFailure: command === "run"
38
+ ? shouldStopRuntime(topology.primary.config.lifecycle.run)
39
+ : command === "start" &&
40
+ shouldStopRuntime(topology.primary.config.lifecycle.open),
41
+ execute: async (target) => {
42
+ const confirmation = session.targetConfirmations?.get(target.configPath);
43
+ return runManager({
44
+ action: command,
45
+ flags,
46
+ configPath: target.configPath,
47
+ ...(confirmation === undefined ? {} : { confirmation }),
48
+ ...(topology.targets.length > 1 ? { targetName: target.name } : {}),
49
+ operationId,
50
+ ...engineBinding,
51
+ configSha256: runtimeConfigurationSha256(target.config),
52
+ });
53
+ },
54
+ stop: (target) => runManager({
55
+ action: "stop",
56
+ flags,
57
+ configPath: target.configPath,
58
+ targetName: target.name,
59
+ operationId,
60
+ ...engineBinding,
61
+ configSha256: runtimeConfigurationSha256(target.config),
62
+ }),
63
+ });
64
+ const release = ["stop", "discard"].includes(command)
65
+ ? releaseOwnedColimaWhenIdle({
66
+ projectRoot: topology.projectRoot,
67
+ targets: topology.targets,
68
+ })
69
+ : null;
70
+ const withRelease = (receipt) => release
71
+ ? {
72
+ ...receipt,
73
+ output: [receipt.output, release.message]
74
+ .filter(Boolean)
75
+ .join("\n"),
76
+ }
77
+ : receipt;
78
+ if (results.length === 1)
79
+ return { topology, runtime: withRelease(results[0]) };
80
+ return {
81
+ topology,
82
+ runtime: {
83
+ action: command,
84
+ durationMs: Math.round(results.reduce((total, result) => total + result.durationMs, 0) *
85
+ 100) / 100,
86
+ output: [
87
+ results
88
+ .map((result) => `[${result.target}]\n${result.output}`)
89
+ .join("\n\n"),
90
+ release?.message,
91
+ ]
92
+ .filter(Boolean)
93
+ .join("\n\n"),
94
+ targets: results,
95
+ },
96
+ };
97
+ };
98
+ return withRuntimeWorkloadAdmission({
99
+ policy: topology.primary.config.resources.workloadQueue,
100
+ command,
101
+ json: flags.json,
102
+ execute: executeStack,
103
+ });
104
+ };
@@ -0,0 +1,8 @@
1
+ /** Bind configured lifecycle-only admission without queuing proofs or interactive sessions. */
2
+ import type { RehearsalResourcePolicy } from "../project/configuration_contract.mjs";
3
+ export declare const withRuntimeWorkloadAdmission: <Result>({ policy, command, json, execute, }: {
4
+ policy: RehearsalResourcePolicy["workloadQueue"];
5
+ command: string;
6
+ json: boolean;
7
+ execute: () => Promise<Result>;
8
+ }) => Promise<Result>;
@@ -0,0 +1,22 @@
1
+ import { getOperationCancellationSignal } from "../shared/cancellation.mjs";
2
+ import { withWorkloadAdmission } from "../runtime/workload_queue.mjs";
3
+ export const withRuntimeWorkloadAdmission = async ({ policy, command, json, execute, }) => {
4
+ if (!policy?.enabled ||
5
+ (command !== "start" &&
6
+ command !== "run" &&
7
+ command !== "reset" &&
8
+ command !== "migrate"))
9
+ return execute();
10
+ const signal = getOperationCancellationSignal();
11
+ return withWorkloadAdmission({
12
+ enabled: true,
13
+ command,
14
+ waitTimeoutSeconds: policy.waitTimeoutSeconds ?? 30,
15
+ ...(signal ? { signal } : {}),
16
+ onWaiting: () => {
17
+ if (!json)
18
+ process.stderr.write("Waiting for another participating local Rehearsal runtime phase. Ctrl+C cancels this request.\n");
19
+ },
20
+ execute,
21
+ });
22
+ };
@@ -67,6 +67,10 @@ export declare const loadPreparationContext: (planOptions: RehearsalConfigPathOp
67
67
  retainBaselineGenerations: number;
68
68
  }>;
69
69
  resources: Readonly<{
70
+ workloadQueue?: {
71
+ enabled: boolean;
72
+ waitTimeoutSeconds: number;
73
+ };
70
74
  staleAfterSeconds: number | null;
71
75
  retainedEnvironmentCount: number;
72
76
  maximumBytes: number | null;
@@ -245,6 +249,10 @@ export declare const buildRefreshWorkflowPlan: (planOptions: RehearsalConfigPath
245
249
  retainBaselineGenerations: number;
246
250
  }>;
247
251
  resources: Readonly<{
252
+ workloadQueue?: {
253
+ enabled: boolean;
254
+ waitTimeoutSeconds: number;
255
+ };
248
256
  staleAfterSeconds: number | null;
249
257
  retainedEnvironmentCount: number;
250
258
  maximumBytes: number | null;
@@ -0,0 +1,2 @@
1
+ import { type CommandDefinition } from "./command_contract.mjs";
2
+ export declare const createWorkloadCommandHandlers: () => readonly CommandDefinition[];
@@ -0,0 +1,38 @@
1
+ /** Inspect/recover queue metadata without needing Docker, config or a project lock. */
2
+ import { inspectWorkloads, recoverWorkloads, withWorkloadDiagnostics, } from "../runtime/workload_queue.mjs";
3
+ import { exactCommands, renderedCommand, } from "./command_contract.mjs";
4
+ import { RehearsalError } from "../shared/diagnostics.mjs";
5
+ export const createWorkloadCommandHandlers = () => [
6
+ exactCommands("workloads", ["workloads"], async () => renderedCommand({
7
+ data: await withWorkloadDiagnostics(() => inspectWorkloads()),
8
+ render: (result) => [
9
+ "LOCAL WORKLOAD QUEUE — READ ONLY",
10
+ "Participating lifecycle phases only. Host/VM capacity is not measured.",
11
+ ...result.entries.map((entry) => `${entry.command} · ${entry.phase} · owner ${entry.owner} · PID ${entry.pid}${entry.recoveryRequired ? " · recovery required" : ""}`),
12
+ result.entries.length
13
+ ? ""
14
+ : "No participating workload is queued or held.",
15
+ ...(result.recoveryDigest
16
+ ? [
17
+ "A dead holder may have surviving children. Check its owned commands before recovery.",
18
+ `After checking: rehearsal workloads recover --confirm-workloads=${result.recoveryDigest}`,
19
+ "Recovery clears exactly reviewed queue metadata; it does not stop processes or change databases.",
20
+ ]
21
+ : []),
22
+ ]
23
+ .filter(Boolean)
24
+ .join("\n"),
25
+ })),
26
+ exactCommands("workloads-recover", ["workloads recover"], async ({ flags }) => {
27
+ if (!flags.workloadConfirmation)
28
+ throw new RehearsalError({
29
+ category: "configuration_invalid",
30
+ code: "CONFIGURATION_INVALID",
31
+ message: "Inspect rehearsal workloads and review dead holders before using --confirm-workloads=<digest>. Check their owned commands first; recovery does not stop surviving children.",
32
+ });
33
+ return renderedCommand({
34
+ data: await withWorkloadDiagnostics(() => recoverWorkloads(flags.workloadConfirmation)),
35
+ render: (result) => `Recovered ${result.recovered} reviewed dead workload holder(s). No process, container or database was changed.`,
36
+ });
37
+ }),
38
+ ];
@@ -4,6 +4,11 @@ export type RunPersistenceMode = "keep-until-stop" | "stop-after-run";
4
4
  export type OpenPersistenceMode = "keep-until-stop" | "stop-on-application-exit";
5
5
  export type ApplicationRequiredService = "database" | "auth" | "storage" | "postgrest" | "gateway" | "mail" | "studio" | "realtime" | "edge-functions" | "analytics" | "image-transforms";
6
6
  export interface RehearsalResourcePolicy {
7
+ /** Cooperatively serialize participating local runtime lifecycle phases. */
8
+ workloadQueue?: {
9
+ enabled: boolean;
10
+ waitTimeoutSeconds?: number;
11
+ };
7
12
  /** Review cutoff based on container start/stop, not application or database use. */
8
13
  staleAfterSeconds?: number | null;
9
14
  /** Advisory limit; retained databases are never automatically deleted. */
@@ -54,6 +54,10 @@ export declare const normalizeRehearsalConfig: (input: unknown) => Readonly<{
54
54
  retainBaselineGenerations: number;
55
55
  }>;
56
56
  resources: Readonly<{
57
+ workloadQueue?: {
58
+ enabled: boolean;
59
+ waitTimeoutSeconds: number;
60
+ };
57
61
  staleAfterSeconds: number | null;
58
62
  retainedEnvironmentCount: number;
59
63
  maximumBytes: number | null;
@@ -54,6 +54,10 @@ export declare const loadRehearsalConfig: ({ projectRoot, configPath, }?: Rehear
54
54
  retainBaselineGenerations: number;
55
55
  }>;
56
56
  resources: Readonly<{
57
+ workloadQueue?: {
58
+ enabled: boolean;
59
+ waitTimeoutSeconds: number;
60
+ };
57
61
  staleAfterSeconds: number | null;
58
62
  retainedEnvironmentCount: number;
59
63
  maximumBytes: number | null;
@@ -3,6 +3,10 @@ import type { ApplicationRequiredService } from "./configuration_contract.mjs";
3
3
  import { type PlainObject } from "./configuration_validation.mjs";
4
4
  export declare const APPLICATION_REQUIRED_SERVICES: readonly ["database", "auth", "storage", "postgrest", "gateway", "mail", "studio", "realtime", "edge-functions", "analytics", "image-transforms"];
5
5
  export declare const normalizeResourcePolicy: (resources: PlainObject) => Readonly<{
6
+ workloadQueue?: {
7
+ enabled: boolean;
8
+ waitTimeoutSeconds: number;
9
+ };
6
10
  staleAfterSeconds: number | null;
7
11
  retainedEnvironmentCount: number;
8
12
  maximumBytes: number | null;