You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
state.json's `phase` field reflects the **last entered** phase, not the
**currently active** phase. The engine writes it once on each
`transition()`; artifact writes within a phase do not refresh it. So
between phase transitions an operator polling state.json sees stale
labels — including a phase whose artifacts have already been written
to disk. From the issue:
> In one observed run on `reflective@76ed590`:
>
> | Time | `state.json.phase` | iter-1 artifacts on disk |
> |---|---|---|
> | 0:00 | INIT | (none) |
> | 0:14 | DESIGN | bundle.yaml, problem.md written at 0:14 |
> | 0:42 | DESIGN (still!) | findings.json written at 0:42 |
> | 0:51 | EXECUTE_ANALYZE | first state update post-DESIGN |
>
> Between 0:14 and 0:51, state.json claimed phase=DESIGN despite
> EXECUTE_ANALYZE having long since started AND completed.
Renaming the on-disk key to `last_entered_phase` makes the entry-only
semantics unambiguous. The schema description, the data-model doc, and
the architecture doc all spell out that operators wanting fine-grained
progress should watch the iteration artifact directory's mtimes rather
than `state.json`.
## What changed
- `orchestrator.engine` writes `last_entered_phase` and reads either
key on load, normalizing legacy `phase` → `last_entered_phase` in
memory. Migration happens automatically on the next `transition()`
or `force_phase()`, which drops the legacy key. No flag day, no
separate migration script — in-flight pre-AI-native-Systems-Research#236 runs converge on
their next phase advance.
- `Engine.last_entered_phase` is the canonical Python property;
`Engine.phase` is kept as an alias for source compatibility (most
internal callers already use it).
- `read_phase_field(state, default=None)` helper centralizes the
backward-compat lookup for code that reads state.json without going
through Engine. Wired into `status.py`, `warm_start.py`, `cli.py`,
and `campaign_index.py`.
- `templates/state.json` and `schemas/state.schema.json` use the
canonical name; the schema description documents entry-only
semantics inline.
- `docs/data-model.md` and `docs/architecture.md` document the
semantics and the migration story.
## What did NOT change
- The orchestrator's behavior. `transition()` still writes on entry
only; `force_phase()` still bumps `iteration`. The fix is a renaming
and a documentation pass — exactly what the issue asked for.
## Tests
- New `TestLastEnteredPhaseRename` (8 tests) covers the rename,
backward-compat read, automatic migration on next save, both helper
branches, and the missing-keys / unrecognized-phase error paths.
- Existing engine assertions on `saved["phase"]` updated to
`saved["last_entered_phase"]`. Fixture-style `"phase":` keys in
test_campaign.py / test_schemas.py / test_critic.py / test_pre_work.py
/ test_templates.py updated to the canonical name (the legacy key is
exercised in the dedicated rename tests).
- End-to-end smoke check confirms a synthetic pre-AI-native-Systems-Research#236 state.json loads,
reports the correct phase via both `Engine.phase` and `read_phase_field`,
and migrates to the canonical shape on the next transition.
1182 passed, 2 skipped. No regressions.
ClosesAI-native-Systems-Research#236.
Refs AI-native-Systems-Research#228 (isolation tracker).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
-`transition(to_state)` validates against the transition table, updates the timestamp, and atomically writes `state.json`.
87
+
-`transition(to_state)` validates against the transition table, updates the timestamp, sets `last_entered_phase`, and atomically writes `state.json`. Both fields update only on phase entry — artifact writes within a phase do not refresh them (#236), so operators polling `state.json` for progress see entry-time values linger throughout long phases. Watch artifact mtimes for sub-second progress instead.
88
88
- Iteration counter increments only on the DONE → DESIGN transition (starting a new iteration). Loopbacks from HUMAN_DESIGN_GATE → DESIGN (reject) do NOT increment — they are revisions within the same iteration.
89
89
- The DONE state allows transition to DESIGN for the next iteration.
Copy file name to clipboardExpand all lines: docs/data-model.md
+5-3Lines changed: 5 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -52,19 +52,21 @@ The campaign configuration. Describes the target system and points to prompt lay
52
52
53
53
**Schema:**`schemas/state.schema.json`
54
54
55
-
A bookmark. It tells the orchestrator what phase we're in, which iteration we're on, and what we're investigating. If the process crashes, it resumes from here.
55
+
A bookmark. It tells the orchestrator what phase we entered last, which iteration we're on, and what we're investigating. If the process crashes, it resumes from here.
56
56
57
57
| Field | What it means |
58
58
|---|---|
59
-
|`phase`|Which step of the loop (INIT, DESIGN, HUMAN_DESIGN_GATE, EXECUTE_ANALYZE, HUMAN_FINDINGS_GATE, DONE) |
59
+
|`last_entered_phase`|The last phase the engine entered (INIT, PRE_WORK, DESIGN, CRITIC, HUMAN_DESIGN_GATE, EXECUTE_ANALYZE, HUMAN_FINDINGS_GATE, DONE). **Not** necessarily the currently active phase — see the caveat below (#236).|
60
60
|`iteration`| How many times we've gone around the loop (0 = haven't started yet) |
61
61
|`run_id`| A name for this campaign |
62
62
|`family`| What mechanism we're currently exploring (e.g., "routing-signals") |
63
-
|`timestamp`| When this was last updated |
63
+
|`timestamp`| When this was last updated (i.e., when `last_entered_phase` was last entered — *not* when an artifact was last written) |
64
64
|`config_ref`| Path to the campaign configuration file (null before setup) |
65
65
66
66
The orchestrator writes this atomically (temp file + rename) so a crash never leaves a corrupt checkpoint.
67
67
68
+
**Entry-only semantics (#236).**`last_entered_phase` and `timestamp` are updated *only* when the engine transitions into a new phase. Artifact writes within a phase do not update them. So during a long phase, you'll see the entry-time values linger even though the phase is well underway and may have already produced artifacts. If you need a sub-second progress signal (e.g., for a status dashboard), watch the iteration artifact directory's mtimes (`runs/iter-N/*.json`, `runs/iter-N/*.yaml`) rather than `state.json`. The field was renamed from `phase` in #236 to make the entry-only semantics unambiguous; `orchestrator.engine` accepts the legacy key on load (in-flight pre-#236 runs) and migrates it to the canonical name on the next save.
69
+
68
70
## 2. ledger.json — "What happened in each iteration?"
"description": "Current phase, family, and iteration — the investigation checkpoint.",
5
+
"description": "Last-entered phase, family, and iteration — the investigation checkpoint. The phase field reflects what was last entered; it is not updated as artifacts are written within a phase. Operators wanting fine-grained progress should watch the iteration artifact directory's mtimes (#236).",
"description": "Current state machine phase. PRE_WORK (issue #167) is an opt-in cheap-exploration phase between INIT and DESIGN. CRITIC (issue #87) is an opt-in falsifiability check between DESIGN and HUMAN_DESIGN_GATE."
16
+
"description": "The last phase the engine entered (#236). NOT necessarily the currently active phase — artifact writes within a phase do not update this field, so during a long phase you'll see the entry-time value linger even though the phase is well underway. Renamed from `phase` in #236 so its semantics aren't conflated with 'currently active phase.' Pre-#236 state.json files used the key `phase` instead; ``orchestrator.engine`` accepts those for backward compat (in-flight runs) and migrates them to this key on the next save, but the schema only describes the canonical post-#236 shape. PRE_WORK (#167) is an opt-in cheap-exploration phase between INIT and DESIGN. CRITIC (#87) is an opt-in falsifiability check between DESIGN and HUMAN_DESIGN_GATE."
0 commit comments