Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 53 additions & 7 deletions .harness/docs/ARCHITECTURE.md

Large diffs are not rendered by default.

447 changes: 441 additions & 6 deletions orchestration/story_coordinator.py

Large diffs are not rendered by default.

16 changes: 13 additions & 3 deletions scripts/l5-run
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
#!/usr/bin/env python3
"""Execute an approved story through the story workflow.

Usage: l5-run <story-id>
Usage: l5-run <story-id> [--stage <stage>]
The target repository is found by walking up from the current directory
to the nearest .harness/config.yaml.

A run whose state.json says it escalated resumes at the recorded stage.
--stage overrides that with a stage the loaded workflow defines; the
coordinator refuses one it does not.
"""
import sys
from pathlib import Path
Expand All @@ -16,11 +20,17 @@ import story_coordinator # noqa: E402


def main() -> int:
if len(sys.argv) != 2:
argv = sys.argv[1:]
stage = None
if len(argv) == 3 and argv[1] == "--stage":
argv, stage = argv[:1], argv[2]
if len(argv) != 1:
print(__doc__.strip(), file=sys.stderr)
return 1
target_root = harness_config.find_target_root(Path.cwd())
return story_coordinator.run_story(sys.argv[1], HARNESS_ROOT, target_root)
return story_coordinator.run_story(
argv[0], HARNESS_ROOT, target_root, start_stage=stage
)


if __name__ == "__main__":
Expand Down
9 changes: 9 additions & 0 deletions tests/test_coordinator_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@

# Every status the coordinator may write. A run starts `running` and ends in
# one of ENDING_STATUSES; the source check below fails if a fourth appears.
# Ending is not the same as final: since story-020 an `escalated` run can be
# resumed, which returns its status to `running`. `completed` is the one a
# rerun still refuses.
ENDING_STATUSES = {"completed", "escalated"}
STATUSES = {"running", *ENDING_STATUSES}

Expand Down Expand Up @@ -97,6 +100,12 @@ def state_contract_problems(state: dict) -> list[str]:
"retry_count": int,
"verification_iterations": int,
"artifacts": list,
# story-020's resume fields. Each defaults to empty, which is what a
# state file written before this story loads as and what every reader
# treats as "not established".
"story_digest": str,
"escalation_commit": str,
"harness_revision": str,
}
declared = {f.name for f in dataclasses.fields(story_coordinator.RunState)}
problems = []
Expand Down
8 changes: 7 additions & 1 deletion tests/test_story_010_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,12 @@ def test_no_reader_was_pointed_at_the_archive():
assembler = Path(context_assembler.__file__).read_text(encoding="utf-8")
assert "attempts" not in assembler
coordinator = Path(story_coordinator.__file__).read_text(encoding="utf-8")
# story-020 gave the directory its own helper, because the resume has to
# refuse an attempt directory that already exists and naming it a second
# time is how one fact ends up in two places. The guarantee is unchanged
# and now exact: one literal in the module, in the helper both the archive
# and the resume derive the directory from.
body = _archive_code_body("archive_attempt")
assert coordinator.count('"attempts"') == 1
assert "attempts" in body
assert "attempts" in _archive_code_body("attempt_dir")
assert "attempt_dir(" in body
10 changes: 9 additions & 1 deletion tests/test_story_012_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -653,7 +653,15 @@ def test_a_run_whose_retry_history_keeps_disappearing_routes_identically(

run_dir, control_dir = run_dir_of(target_root), run_dir_of(control_root)
assert runner.calls == control.calls
assert read_state(run_dir) == read_state(control_dir)
# Every field but the escalation commit, which since story-020 records the
# commit each escalation makes on its own branch: these are two copies of
# one repository, so the two shas differ for a reason that has nothing to
# do with routing. Compared field by field so a new field is included by
# default rather than needing to be added here.
volatile = {"escalation_commit"}
assert ({k: v for k, v in read_state(run_dir).items() if k not in volatile}
== {k: v for k, v in read_state(control_dir).items()
if k not in volatile})
assert _log_messages(run_dir) == _log_messages(control_dir)
# And the control run did keep a full history, so the comparison above is
# between a run missing the artifact and one that had it.
Expand Down
12 changes: 9 additions & 3 deletions tests/test_story_019_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1124,8 +1124,12 @@ def test_the_capture_names_no_stage_and_no_prefix():
# The baseline is evidence, never state
# --------------------------------------------------------------------------

#: state.json's fields, written here rather than read off the dataclass that
#: produces them — a comparison against its own source could not fail.
#: state.json's fields as of story-019, written here rather than read off the
#: dataclass that produces them — a comparison against its own source could not
#: fail. A later story may add a field for its own reasons: story-020 added
#: three so a resume can tell what has changed since a run escalated. What this
#: story claims is narrower and is what the assertion below now states — the
#: *baseline* added none, and none of the fields that arrived later names it.
STATE_FIELDS = {"story_id", "branch", "status", "current_stage", "retry_count",
"verification_iterations", "artifacts"}

Expand All @@ -1135,7 +1139,9 @@ def test_state_json_gains_no_field_and_never_names_the_baseline(
):
assert run(target, harness_root, RETRY_SHAPE, [FAIL, PASS])[0] == 0
state_text = (run_dir_of(target) / "state.json").read_text()
assert set(json.loads(state_text)) == STATE_FIELDS
fields = set(json.loads(state_text))
assert STATE_FIELDS <= fields
assert [name for name in fields if "baseline" in name] == []
assert BASELINE not in state_text
# The control: the run this state describes did capture a baseline, so
# the absence above is about state.json rather than about a run that
Expand Down
Loading
Loading