prizmkit 1.1.152 → 1.1.154

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 (44) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/README.md +100 -87
  3. package/bundled/dev-pipeline/assets/skill-subagent-integration.md +1 -1
  4. package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +14 -0
  5. package/bundled/dev-pipeline/prizmkit_runtime/cli.py +192 -110
  6. package/bundled/dev-pipeline/prizmkit_runtime/commands.py +146 -111
  7. package/bundled/dev-pipeline/prizmkit_runtime/daemon.py +3 -5
  8. package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +0 -1
  9. package/bundled/dev-pipeline/prizmkit_runtime/paths.py +0 -3
  10. package/bundled/dev-pipeline/prizmkit_runtime/reset.py +122 -15
  11. package/bundled/dev-pipeline/prizmkit_runtime/reset_preserve.py +1 -1
  12. package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +1 -1
  13. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +19 -52
  14. package/bundled/dev-pipeline/prizmkit_runtime/status.py +106 -1
  15. package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +2 -2
  16. package/bundled/dev-pipeline/scripts/parse-stream-progress.py +10 -12
  17. package/bundled/dev-pipeline/scripts/update-bug-status.py +196 -67
  18. package/bundled/dev-pipeline/scripts/update-checkpoint.py +16 -3
  19. package/bundled/dev-pipeline/scripts/update-feature-status.py +45 -117
  20. package/bundled/dev-pipeline/scripts/update-refactor-status.py +45 -119
  21. package/bundled/dev-pipeline/scripts/utils.py +119 -0
  22. package/bundled/dev-pipeline/templates/bug-fix-list-schema.json +3 -2
  23. package/bundled/dev-pipeline/tests/test_auto_skip.py +111 -32
  24. package/bundled/dev-pipeline/tests/test_checkpoint_state.py +148 -12
  25. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +0 -19
  26. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +113 -188
  27. package/bundled/dev-pipeline/tests/test_recovery_workflow.py +211 -0
  28. package/bundled/dev-pipeline/tests/test_reset_modes.py +935 -0
  29. package/bundled/dev-pipeline/tests/test_reset_preserve.py +5 -5
  30. package/bundled/dev-pipeline/tests/test_unified_cli.py +544 -181
  31. package/bundled/skills/_metadata.json +1 -1
  32. package/bundled/skills/bug-planner/references/schema-validation.md +1 -1
  33. package/bundled/skills/bug-planner/scripts/validate-bug-list.py +1 -1
  34. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +21 -13
  35. package/bundled/skills/feature-pipeline-launcher/SKILL.md +22 -14
  36. package/bundled/skills/recovery-workflow/SKILL.md +7 -5
  37. package/bundled/skills/recovery-workflow/evals/evals.json +2 -2
  38. package/bundled/skills/recovery-workflow/references/detection.md +3 -3
  39. package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +1 -1
  40. package/bundled/skills/refactor-pipeline-launcher/SKILL.md +21 -13
  41. package/bundled/templates/project-memory-template.md +19 -11
  42. package/package.json +1 -1
  43. package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +0 -228
  44. package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +0 -767
@@ -1,4 +1,4 @@
1
- """Command contracts for the canonical PrizmKit Python runtime."""
1
+ """Executing command registry for the canonical operation-first runtime."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -11,18 +11,19 @@ from .paths import RuntimePaths, resolve_runtime_paths
11
11
  RUNTIME_STATUS = "python-runtime-canonical"
12
12
  CANONICAL_RUNTIME_NOTE = "The Python CLI and prizmkit_runtime modules are the canonical runtime."
13
13
  PYTHON_CLI = "dev-pipeline/cli.py"
14
+ _FAMILIES = ("feature", "bugfix", "refactor")
14
15
 
15
16
 
16
17
  @dataclass(frozen=True)
17
18
  class RuntimeCommand:
18
- """Unified CLI command mapped to the Python behavior provider."""
19
+ """One public CLI command mapped to an executing or diagnostic handler."""
19
20
 
20
21
  group: str
21
22
  action: str
22
23
  target: str | None
23
- current_entrypoint: str | None
24
- entrypoint_kind: str | None
25
- entrypoint_name: str | None
24
+ current_entrypoint: str
25
+ entrypoint_kind: str = "python-cli"
26
+ entrypoint_name: str = ""
26
27
  transition_status: str = RUNTIME_STATUS
27
28
  notes: tuple[str, ...] = (CANONICAL_RUNTIME_NOTE,)
28
29
 
@@ -54,12 +55,16 @@ class CommandResult:
54
55
 
55
56
  def _runtime_invocation(group: str, action: str, target: str | None = None) -> str:
56
57
  if group == "daemon" and target is not None:
57
- return f"python3 {PYTHON_CLI} daemon {target} {action}"
58
- if group == "reset" and target is not None and action == "reset":
59
- return f"python3 {PYTHON_CLI} reset {target}"
60
- if group == "status" and target is not None:
61
- return f"python3 {PYTHON_CLI} status {target}"
62
- return f"python3 {PYTHON_CLI} {group} {action}"
58
+ command = f"daemon {target} {action}"
59
+ elif group in {"run", "status", "reset"} and target is not None:
60
+ command = f"{group} {target}"
61
+ elif group == "diagnostics":
62
+ command = f"diagnostics {action}"
63
+ elif group == "helper":
64
+ command = "helper"
65
+ else:
66
+ command = f"{group} {action}"
67
+ return f"python3 {PYTHON_CLI} {command}"
63
68
 
64
69
 
65
70
  def _command(
@@ -74,74 +79,24 @@ def _command(
74
79
  target=target,
75
80
  action=action,
76
81
  current_entrypoint=_runtime_invocation(group, action, target),
77
- entrypoint_kind="python-cli",
78
82
  entrypoint_name=group if target is None else f"{group}:{target}",
79
83
  notes=notes,
80
84
  )
81
85
 
82
86
 
83
- def _pipeline_commands() -> tuple[RuntimeCommand, ...]:
84
- commands: list[RuntimeCommand] = []
85
- action_matrix = {
86
- "feature": ("run", "status", "reset", "unskip", "test-cli", "describe"),
87
- "bugfix": ("run", "status", "reset", "unskip", "describe"),
88
- "refactor": ("run", "status", "reset", "unskip", "describe"),
89
- "recovery": ("run", "detect", "describe"),
90
- }
91
- for group, actions in action_matrix.items():
92
- for action in actions:
93
- commands.append(
94
- _command(
95
- group,
96
- action,
97
- notes=(
98
- CANONICAL_RUNTIME_NOTE,
99
- "Foreground and recovery behavior is implemented in Python runtime modules.",
100
- ),
101
- )
102
- )
103
- return tuple(commands)
104
-
105
-
106
- def _reset_commands() -> tuple[RuntimeCommand, ...]:
107
- commands = [
87
+ def _run_commands() -> tuple[RuntimeCommand, ...]:
88
+ return tuple(
108
89
  _command(
109
- "reset",
110
- "reset",
111
- target=target,
90
+ "run",
91
+ "run",
92
+ target=family,
112
93
  notes=(
113
94
  CANONICAL_RUNTIME_NOTE,
114
- "Reset actions are explicit manual operations and are not part of automatic continuation recovery.",
95
+ "Foreground pipeline behavior is implemented in Python runtime modules.",
115
96
  ),
116
97
  )
117
- for target in ("feature", "bugfix", "refactor")
118
- ]
119
- commands.append(
120
- _command(
121
- "reset",
122
- "describe",
123
- notes=(CANONICAL_RUNTIME_NOTE, "Lists destructive Python reset commands without running them."),
124
- )
98
+ for family in _FAMILIES
125
99
  )
126
- return tuple(commands)
127
-
128
-
129
- def _daemon_commands() -> tuple[RuntimeCommand, ...]:
130
- commands: list[RuntimeCommand] = []
131
- for target in ("feature", "bugfix", "refactor"):
132
- for action in ("start", "stop", "status", "logs", "restart", "describe"):
133
- commands.append(
134
- _command(
135
- "daemon",
136
- action,
137
- target=target,
138
- notes=(
139
- CANONICAL_RUNTIME_NOTE,
140
- "Daemon lifecycle behavior is implemented by the Python daemon runtime.",
141
- ),
142
- )
143
- )
144
- return tuple(commands)
145
100
 
146
101
 
147
102
  def _status_commands() -> tuple[RuntimeCommand, ...]:
@@ -149,44 +104,80 @@ def _status_commands() -> tuple[RuntimeCommand, ...]:
149
104
  _command(
150
105
  "status",
151
106
  "status",
152
- target=target,
153
- notes=(CANONICAL_RUNTIME_NOTE, "Status is resolved by the Python foreground runtime."),
107
+ target=family,
108
+ notes=(CANONICAL_RUNTIME_NOTE, "Status is resolved by the canonical family status handler."),
154
109
  )
155
- for target in ("feature", "bugfix", "refactor")
110
+ for family in _FAMILIES
156
111
  ]
157
112
  commands.append(
158
113
  _command(
159
114
  "status",
160
115
  "status",
161
116
  target="all",
162
- notes=(CANONICAL_RUNTIME_NOTE, "Aggregated status is represented at the Python CLI boundary."),
117
+ notes=(CANONICAL_RUNTIME_NOTE, "Aggregates configured family status handlers without mutation."),
163
118
  )
164
119
  )
165
120
  return tuple(commands)
166
121
 
167
122
 
123
+ def _reset_commands() -> tuple[RuntimeCommand, ...]:
124
+ return tuple(
125
+ _command(
126
+ "reset",
127
+ "reset",
128
+ target=family,
129
+ notes=(
130
+ CANONICAL_RUNTIME_NOTE,
131
+ "Reset requires one explicit mode and never starts execution.",
132
+ ),
133
+ )
134
+ for family in _FAMILIES
135
+ )
136
+
137
+
138
+ def _daemon_commands() -> tuple[RuntimeCommand, ...]:
139
+ return tuple(
140
+ _command(
141
+ "daemon",
142
+ action,
143
+ target=family,
144
+ notes=(
145
+ CANONICAL_RUNTIME_NOTE,
146
+ "Daemon lifecycle behavior is implemented by the Python daemon runtime.",
147
+ ),
148
+ )
149
+ for family in _FAMILIES
150
+ for action in ("start", "stop", "status", "logs", "restart")
151
+ )
152
+
153
+
168
154
  def _diagnostic_commands() -> tuple[RuntimeCommand, ...]:
169
155
  return tuple(
170
156
  _command(
171
157
  "diagnostics",
172
158
  action,
173
- notes=(CANONICAL_RUNTIME_NOTE, "Diagnostic commands are implemented in the Python runtime."),
159
+ notes=(CANONICAL_RUNTIME_NOTE, "Diagnostic behavior is implemented in the Python runtime."),
174
160
  )
175
- for action in ("paths", "entrypoints", "version")
161
+ for action in ("paths", "entrypoints", "version", "ai-cli")
176
162
  )
177
163
 
178
164
 
179
165
  COMMAND_REGISTRY: tuple[RuntimeCommand, ...] = (
180
- *_pipeline_commands(),
166
+ *_run_commands(),
167
+ *_status_commands(),
181
168
  *_reset_commands(),
182
169
  *_daemon_commands(),
183
- *_status_commands(),
184
170
  *_diagnostic_commands(),
171
+ _command(
172
+ "helper",
173
+ "run",
174
+ notes=(CANONICAL_RUNTIME_NOTE, "Low-level helper behavior is implemented by runtime_helper.py."),
175
+ ),
185
176
  )
186
177
 
187
178
 
188
179
  def list_runtime_commands() -> tuple[RuntimeCommand, ...]:
189
- """Return the unified CLI command inventory."""
180
+ """Return the public executing/diagnostic command inventory."""
190
181
  return COMMAND_REGISTRY
191
182
 
192
183
 
@@ -199,23 +190,6 @@ def find_runtime_command(group: str, action: str, target: str | None = None) ->
199
190
  raise KeyError(f"Unknown runtime command: group={group}{target_text} action={action}")
200
191
 
201
192
 
202
- def describe_command(command: RuntimeCommand, paths: RuntimePaths | None = None) -> CommandResult:
203
- """Describe a Python runtime command contract without running behavior."""
204
- _ = paths or resolve_runtime_paths()
205
- details = [
206
- f"command: {command.group} {command.target or ''} {command.action}".replace(" ", " ").strip(),
207
- f"runtime status: {command.transition_status}",
208
- f"current entrypoint: {command.current_entrypoint or PYTHON_CLI}",
209
- ]
210
- details.extend(f"note: {note}" for note in command.notes)
211
- return CommandResult(
212
- exit_code=0,
213
- title="Python runtime command contract",
214
- message="This command is available in the canonical PrizmKit Python runtime.",
215
- details=tuple(details),
216
- )
217
-
218
-
219
193
  def handle_runtime_command(
220
194
  group: str,
221
195
  action: str,
@@ -224,33 +198,60 @@ def handle_runtime_command(
224
198
  paths: RuntimePaths | None = None,
225
199
  legacy_args: tuple[str, ...] = (),
226
200
  ) -> CommandResult:
227
- """Handle runtime commands, dispatching ported Python runtime behavior where available."""
201
+ """Dispatch one registered public runtime command to real behavior."""
202
+ find_runtime_command(group, action, target)
228
203
  runtime_paths = paths or resolve_runtime_paths()
229
- if group in {"feature", "bugfix", "refactor"} and action in {"run", "status", "unskip", "test-cli"}:
204
+
205
+ if group == "run" and target in _FAMILIES and action == "run":
230
206
  from .runners import run_pipeline_command
231
207
 
232
- return run_pipeline_command(group, action, legacy_args, runtime_paths)
233
- if group == "status" and target in {"feature", "bugfix", "refactor"} and action == "status":
208
+ return run_pipeline_command(target, "run", legacy_args, runtime_paths)
209
+ if group == "status" and target in _FAMILIES and action == "status":
234
210
  from .runners import run_pipeline_command
235
211
 
236
- return run_pipeline_command(target, action, legacy_args, runtime_paths)
237
- if group == "recovery" and action in {"run", "detect"}:
238
- from .runners import run_recovery_command
212
+ return run_pipeline_command(target, "status", legacy_args, runtime_paths)
213
+ if group == "status" and target == "all" and action == "status":
214
+ if legacy_args:
215
+ return CommandResult(
216
+ 2,
217
+ "Python aggregate status",
218
+ "status all does not accept a plan path or family-specific arguments",
219
+ )
220
+ from .runners import run_pipeline_command
221
+ from .status import aggregate_family_status
239
222
 
240
- return run_recovery_command(action, legacy_args, runtime_paths)
241
- if group == "reset" and target in {"feature", "bugfix", "refactor"} and action == "reset":
223
+ aggregate = aggregate_family_status(
224
+ runtime_paths,
225
+ lambda family: run_pipeline_command(family, "status", (), runtime_paths),
226
+ )
227
+ return CommandResult(
228
+ aggregate.exit_code,
229
+ "Python aggregate status",
230
+ "Read-only status for all pipeline families.",
231
+ stdout=aggregate.output,
232
+ )
233
+ if group == "reset" and target in _FAMILIES and action == "reset":
242
234
  from .reset import run_reset_command
243
235
 
244
236
  return run_reset_command(target, legacy_args, runtime_paths)
245
- if group == "daemon" and target in {"feature", "bugfix", "refactor"} and action in {"start", "stop", "status", "logs", "restart", "describe"}:
237
+ if group == "daemon" and target in _FAMILIES and action in {"start", "stop", "status", "logs", "restart"}:
246
238
  from .daemon import run_daemon_command
247
239
 
248
240
  try:
249
241
  return run_daemon_command(target, action, legacy_args, runtime_paths)
250
242
  except ValueError as exc:
251
243
  return CommandResult(1, f"Python {target} daemon", str(exc), stdout="", stderr=str(exc))
252
- command = find_runtime_command(group, action, target)
253
- return describe_command(command, runtime_paths)
244
+ if group == "helper" and action == "run" and target is None:
245
+ from .runtime_helper import render_result, run_helper_command
246
+
247
+ helper_result = run_helper_command(legacy_args)
248
+ return CommandResult(
249
+ helper_result.exit_code,
250
+ "Python runtime helper",
251
+ "Cross-platform prompt runtime helper result.",
252
+ stdout=render_result(helper_result, json_output="--json" in legacy_args),
253
+ )
254
+ raise RuntimeError(f"Registered command has no handler: {group} {target or ''} {action}".strip())
254
255
 
255
256
 
256
257
  def diagnostics_paths(paths: RuntimePaths | None = None) -> CommandResult:
@@ -275,20 +276,27 @@ def diagnostics_paths(paths: RuntimePaths | None = None) -> CommandResult:
275
276
 
276
277
 
277
278
  def diagnostics_entrypoints(paths: RuntimePaths | None = None) -> CommandResult:
278
- """Return an inventory of canonical Python runtime and utility entrypoints."""
279
+ """Return canonical runtime, command, and utility entrypoints."""
279
280
  runtime_paths = paths or resolve_runtime_paths()
280
281
  details: list[str] = [
281
282
  f"runtime:cli: {runtime_paths.pipeline_root / 'cli.py'}",
282
283
  f"runtime:module: {runtime_paths.pipeline_root / 'prizmkit_runtime'}",
283
284
  f"runtime:helper-module: {runtime_paths.pipeline_root / 'prizmkit_runtime' / 'runtime_helper.py'}",
284
285
  ]
286
+ details.extend(
287
+ f"command:{command.group}:{command.target or command.action}: {command.current_entrypoint}"
288
+ for command in COMMAND_REGISTRY
289
+ )
285
290
  for name, relative_path in sorted(UTILITY_ENTRYPOINTS.items()):
286
291
  invocation = describe_existing_entrypoint("utility", name, paths=runtime_paths)
287
- details.append(f"utility:{name}: {invocation.script_path if invocation else runtime_paths.pipeline_root / relative_path}")
292
+ details.append(
293
+ f"utility:{name}: "
294
+ f"{invocation.script_path if invocation else runtime_paths.pipeline_root / relative_path}"
295
+ )
288
296
  return CommandResult(
289
297
  exit_code=0,
290
298
  title="Python runtime entrypoint inventory",
291
- message="Inventory for canonical Python runtime and utility scripts.",
299
+ message="Inventory for canonical Python runtime commands and utility scripts.",
292
300
  details=tuple(details),
293
301
  )
294
302
 
@@ -303,12 +311,39 @@ def diagnostics_version() -> CommandResult:
303
311
  )
304
312
 
305
313
 
314
+ def diagnostics_ai_cli(paths: RuntimePaths | None = None) -> CommandResult:
315
+ """Report the family-neutral AI CLI resolution used by runtime commands."""
316
+ from .config import load_runtime_config
317
+
318
+ runtime_paths = paths or resolve_runtime_paths()
319
+ config = load_runtime_config(runtime_paths)
320
+ cli = config.ai_client.command or "(not found)"
321
+ details = [
322
+ f"detected_cli: {cli}",
323
+ f"platform: {config.ai_client.platform or 'unresolved'}",
324
+ f"source: {config.ai_client.source}",
325
+ f"runtime_mode: {config.runtime_mode}",
326
+ ]
327
+ if config.model:
328
+ details.append(f"model: {config.model}")
329
+ details.extend(f"note: {note}" for note in config.ai_client.notes)
330
+ return CommandResult(
331
+ 0 if config.ai_client.command else 1,
332
+ "Python runtime AI CLI diagnostics",
333
+ "AI CLI resolution for the canonical Python runtime.",
334
+ tuple(details),
335
+ )
336
+
337
+
306
338
  def handle_diagnostics(action: str, paths: RuntimePaths | None = None) -> CommandResult:
307
- """Dispatch diagnostics actions."""
339
+ """Dispatch one registered diagnostics action."""
340
+ find_runtime_command("diagnostics", action)
308
341
  if action == "paths":
309
342
  return diagnostics_paths(paths)
310
343
  if action == "entrypoints":
311
344
  return diagnostics_entrypoints(paths)
312
345
  if action == "version":
313
346
  return diagnostics_version()
314
- raise KeyError(f"Unknown diagnostics action: {action}")
347
+ if action == "ai-cli":
348
+ return diagnostics_ai_cli(paths)
349
+ raise RuntimeError(f"Registered diagnostics action has no handler: {action}")
@@ -70,7 +70,7 @@ ID_KEY_BY_KIND = {
70
70
 
71
71
  PROGRESS_COUNTS_BY_KIND = {
72
72
  "feature": ("completed", "in_progress", "failed", "pending", "skipped", "auto_skipped"),
73
- "bugfix": ("completed", "in_progress", "failed", "pending", "needs_info", "skipped"),
73
+ "bugfix": ("completed", "in_progress", "failed", "pending", "needs_info", "skipped", "auto_skipped"),
74
74
  "refactor": ("completed", "in_progress", "failed", "pending", "skipped", "auto_skipped"),
75
75
  }
76
76
 
@@ -79,8 +79,6 @@ def run_daemon_command(kind: str, action: str, legacy_args: tuple[str, ...], pat
79
79
  """Run a daemon lifecycle command for feature, bugfix, or refactor."""
80
80
  family = family_for(kind, paths)
81
81
  daemon_paths = _daemon_paths(family)
82
- if action == "describe":
83
- return CommandResult(0, f"Python {kind} daemon", _help_text(family))
84
82
  if action == "start":
85
83
  return _start(family, daemon_paths, legacy_args, paths)
86
84
  if action == "status":
@@ -251,7 +249,7 @@ def _parse_start_args(family: RunnerFamily, legacy_args: tuple[str, ...]) -> Dae
251
249
  index = 0
252
250
  while index < len(legacy_args):
253
251
  arg = legacy_args[index]
254
- if arg in {"--help", "-h", "help"}:
252
+ if arg in {"--help", "-h"}:
255
253
  help_requested = True
256
254
  elif arg == "--env" and index + 1 < len(legacy_args):
257
255
  index += 1
@@ -300,8 +298,8 @@ def _payload_command(family: RunnerFamily, paths, list_path: Path, options: Daem
300
298
  str(paths.project_root),
301
299
  "--pipeline-root",
302
300
  str(paths.pipeline_root),
303
- family.kind,
304
301
  "run",
302
+ family.kind,
305
303
  str(list_path),
306
304
  ]
307
305
  if family.kind == "feature" and options.features_filter:
@@ -16,7 +16,6 @@ UTILITY_ENTRYPOINTS = {
16
16
  "detect-stuck": "scripts/detect-stuck.py",
17
17
  "generate-bootstrap-prompt": "scripts/generate-bootstrap-prompt.py",
18
18
  "generate-bugfix-prompt": "scripts/generate-bugfix-prompt.py",
19
- "generate-recovery-prompt": "scripts/generate-recovery-prompt.py",
20
19
  "generate-refactor-prompt": "scripts/generate-refactor-prompt.py",
21
20
  "init-bugfix-pipeline": "scripts/init-bugfix-pipeline.py",
22
21
  "init-change-artifact": "scripts/init-change-artifact.py",
@@ -23,7 +23,6 @@ class RuntimePaths:
23
23
  feature_state_dir: Path
24
24
  bugfix_state_dir: Path
25
25
  refactor_state_dir: Path
26
- recovery_state_dir: Path
27
26
  layout: str
28
27
 
29
28
  def path_values(self) -> tuple[Path, ...]:
@@ -42,7 +41,6 @@ class RuntimePaths:
42
41
  self.feature_state_dir,
43
42
  self.bugfix_state_dir,
44
43
  self.refactor_state_dir,
45
- self.recovery_state_dir,
46
44
  )
47
45
 
48
46
 
@@ -105,6 +103,5 @@ def resolve_runtime_paths(
105
103
  feature_state_dir=state_dir / "features",
106
104
  bugfix_state_dir=state_dir / "bugfix",
107
105
  refactor_state_dir=state_dir / "refactor",
108
- recovery_state_dir=state_dir / "recovery",
109
106
  layout=layout,
110
107
  )