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
2 changes: 1 addition & 1 deletion .harness/docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ l5 is a level 3 agentic harness: a story execution system. The workflow defines

`story-workflow.json` defines the execution structure: stage list (implementer, tester, verifier, documenter), the prompt and expected artifacts for each stage, the retry routing table (which category of defect returns execution to which stage), and the escalation rule (retries exhausted → escalate). It carries no retry ceiling: since story-028 `max_retries` lives only in `rules/execution-rules.json` — see "Where a retry goes" below.

A stage that writes to the repository declares an optional `changed_files` key naming its changed-files record: the implementer declares `changed-files.json`, the tester declares `tester-changed-files.json`. After any stage with this declaration completes, the coordinator checks that record against `blocked_paths` and escalates on violation — enforcement is driven by the workflow definition, with no stage names hard-coded in the coordinator. The documenter declares no record and is intentionally unchecked; enabling it later is a one-line workflow change. Both records share one schema definition (`modified`/`created`/`deleted` arrays), not two copies.
A stage that writes to the repository declares an optional `changed_files` key naming its changed-files record: the implementer declares `changed-files.json`, the tester declares `tester-changed-files.json`, and since story-044 the documenter declares `documenter-changed-files.json`. After any stage with this declaration completes, the coordinator checks that record against `blocked_paths` and escalates on violation — enforcement is driven by the workflow definition, with no stage names hard-coded in the coordinator. Adding the documenter proved that: the diff that enabled the check touched no file under `orchestration/`, only the stage's `outputs`, `changed_files` and `schemas` entries in the workflow and the prompt paragraph asking for the record. The cost landed instead in the fixtures — the record became a required artifact, so every fake runner in `tests/` that drives the shipped workflow through the documenter had to start writing it. The declaration turns on the required-artifact check and the schema check along with the blocked-path check; it does not bring `may_not_create`, a revert check or a stage baseline, which remain separate declarations the documenter does not carry. All three records share one schema definition (`modified`/`created`/`deleted` arrays), not three copies. The documenter's record is written outward and read by nothing: no stage runs after it, so unlike the implementer's record — injected into later templates through `{{changed_files}}` — it is checked and then only committed.

A stage may also declare an optional `may_not_create` key, a list of repository-relative path prefixes it is not allowed to add files under. The implementer declares `["tests/"]`; no other stage does. After a stage that declares both `changed_files` and `may_not_create`, the coordinator reads that stage's own record and escalates when any entry in its **`created`** array falls under a declared prefix. `modified` and `deleted` are not examined by *this* check — the rule is about independence, not about directories: an implementer must be able to update an existing test whose call site its own signature change broke, but validation it authors itself checks what it built rather than what was asked. Since story-017 those two arrays are decided by the revert check below rather than left unexamined. As with blocked paths, no stage name and no prefix appears in orchestration code; both are read off the stage dict.

Expand Down
14 changes: 12 additions & 2 deletions prompts/documenter.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,19 @@ Do not:
- create tests, or
- rewrite documentation sections the story did not affect.

When you finish, write this file to the run directory at {{run_dir}}:
When you finish, write these files to the run directory at {{run_dir}}:

documentation-report.md: which documents you updated and why, or a
statement that no documentation change was needed and why.

documenter-changed-files.json, your own record of every repository file
this stage touched — the documents you edited, and nothing another stage
edited. This is the record you write outward; it is not the injected
"Changed files" below, which is the implementer's record arriving inward.
It must satisfy this schema:

{{changed_files_schema}}

[Workflow Layer]
Documentation is architectural memory. Future planning agents load these
documents before generating story plans, so record what they will need.
Expand All @@ -36,7 +44,9 @@ injected content as authoritative.
Story:
{{story}}

Changed files:
Changed files — the implementer's record, injected inward for you to read.
It is not the record you are asked to write; that one is
documenter-changed-files.json, described above:
{{changed_files}}

Implementation summary:
Expand Down
11 changes: 9 additions & 2 deletions tests/test_artifact_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ def test_an_extra_key_anywhere_validates(harness_root):

VALID_CHANGED_FILES = {"modified": ["src/app.py"], "created": [], "deleted": []}
VALID_TESTER_RECORD = {"modified": [], "created": ["tests/test_app.py"], "deleted": []}
VALID_DOCUMENTER_RECORD = {"modified": [], "created": [], "deleted": []}
VALID_TEST_RESULTS = {"status": "passed", "tests_run": 1, "tests_passed": 1}
VALID_VERDICT = {
"status": "passed",
Expand Down Expand Up @@ -281,6 +282,9 @@ def __call__(self, prompt, *, stage, cwd, log_path, permission_mode, model,
self._payload("verification-result.json", VALID_VERDICT))
elif stage == "documenter":
(self.run_dir / "documentation-report.md").write_text("n/a\n")
_write(self.run_dir / "documenter-changed-files.json",
self._payload("documenter-changed-files.json",
VALID_DOCUMENTER_RECORD))
return AgentResult(ok=True, result_text=f"{stage} ok")


Expand Down Expand Up @@ -445,8 +449,11 @@ def test_prompts_this_story_leaves_alone_carry_no_schema_placeholders(harness_ro
# planner.md left this list in story-008, which injects {{story_schema}}
# there deliberately; tests/test_story_008_validation.py holds that
# property now. The assertion is unchanged for the prompts still without
# schema injection.
for name in ("assist.md", "documenter.md", "harness-layer.md"):
# schema injection. documenter.md left the list in story-044, which gave
# the documenter a changed-files record and so injects
# {{changed_files_schema}} there deliberately; the stage/schema sweep above
# holds that property, because the documenter now declares a schemas map.
for name in ("assist.md", "harness-layer.md"):
text = (harness_root / "prompts" / name).read_text()
assert "_schema}}" not in text, name

Expand Down
2 changes: 2 additions & 0 deletions tests/test_attempt_archiving.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ def __call__(self, prompt, *, stage, cwd, log_path, permission_mode, model,
elif stage == "documenter":
(self.run_dir / "documentation-report.md").write_text(
f"Documented after attempt {self.attempt}.\n", encoding="utf-8")
write_json(self.run_dir / "documenter-changed-files.json",
{"modified": [], "created": [], "deleted": []})
return AgentResult(ok=True, result_text=f"{stage} done")


Expand Down
Loading
Loading