MCPcopy Index your code
hub / github.com/github/spec-kit / RunState

Class RunState

src/specify_cli/workflows/engine.py:367–561  ·  view source on GitHub ↗

Manages workflow run state for persistence and resume.

Source from the content-addressed store, hash-verified

365
366
367class RunState:
368 """Manages workflow run state for persistence and resume."""
369
370 # ``run_id`` is interpolated into a filesystem path (``runs/<run_id>``)
371 # by both ``save()`` and ``load()``. Constrain it to a charset that
372 # cannot contain path separators (``/`` ``\``), parent-directory
373 # segments (``..``), or NULs — anything that could escape the
374 # ``.specify/workflows/runs/`` directory or be mis-interpreted by the
375 # filesystem. The first-character anchor blocks IDs that start with
376 # ``-`` (which would be mistaken for a CLI flag in error messages
377 # and shell completions).
378 _RUN_ID_PATTERN = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_-]*$")
379
380 @classmethod
381 def _validate_run_id(cls, run_id: str) -> None:
382 """Raise ``ValueError`` if ``run_id`` is not a safe path component.
383
384 This is the single source of truth for what counts as a valid
385 ``run_id``. ``__init__`` calls it to reject malformed IDs at
386 construction time; ``load`` calls it *before* interpolating the
387 ID into a path so a malicious value cannot probe or read files
388 outside ``.specify/workflows/runs/<run_id>/``.
389 """
390 if not isinstance(run_id, str) or not cls._RUN_ID_PATTERN.match(run_id):
391 raise ValueError(
392 f"Invalid run_id {run_id!r}: must be alphanumeric with "
393 "hyphens/underscores only (and must start with an "
394 "alphanumeric character)."
395 )
396
397 def __init__(
398 self,
399 run_id: str | None = None,
400 workflow_id: str = "",
401 project_root: Path | None = None,
402 ) -> None:
403 # ``run_id is None`` (omitted) → auto-generate. An explicit empty
404 # string is *not* the same as "omitted" and must be validated like
405 # any other caller-provided value — otherwise ``__init__("")``
406 # would silently substitute a UUID while ``load("")`` rejects, and
407 # the two entry points would diverge on the empty-string vector.
408 if run_id is None:
409 self.run_id = str(uuid.uuid4())[:8]
410 else:
411 self.run_id = run_id
412 self._validate_run_id(self.run_id)
413 self.workflow_id = workflow_id
414 self.project_root = project_root or Path(".")
415 self.status = RunStatus.CREATED
416 self.current_step_index = 0
417 self.current_step_id: str | None = None
418 self.step_results: dict[str, dict[str, Any]] = {}
419 # Guards step_results mutation and save() so a concurrent fan-out cannot
420 # mutate the dict while save() is serializing it (which would raise
421 # "dictionary changed size during iteration").
422 self._lock = threading.Lock()
423 # Serializes append_log's list append + log.jsonl write so concurrent
424 # fan-out workers cannot interleave or corrupt log lines. Kept separate

Callers 6

_buildMethod · 0.90
freshMethod · 0.90
test_save_and_loadMethod · 0.90
test_append_logMethod · 0.90
executeMethod · 0.85

Calls

no outgoing calls

Tested by 5

_buildMethod · 0.72
freshMethod · 0.72
test_save_and_loadMethod · 0.72
test_append_logMethod · 0.72