Skip to content

Commit d5764ce

Browse files
sriumcpclaude
andauthored
fix(observability): rename state.json phaselast_entered_phase (#236) (#238)
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-#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-#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. Closes #236. Refs #228 (isolation tracker). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0f12fde commit d5764ce

15 files changed

Lines changed: 227 additions & 47 deletions

docs/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ INIT ──▶ DESIGN ──▶ HUMAN_DESIGN_GATE
8484
- DONE → DESIGN (next iteration, increments counter)
8585

8686
**Key behaviors:**
87-
- `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.
8888
- 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.
8989
- The DONE state allows transition to DESIGN for the next iteration.
9090

docs/data-model.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,19 +52,21 @@ The campaign configuration. Describes the target system and points to prompt lay
5252

5353
**Schema:** `schemas/state.schema.json`
5454

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.
5656

5757
| Field | What it means |
5858
|---|---|
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). |
6060
| `iteration` | How many times we've gone around the loop (0 = haven't started yet) |
6161
| `run_id` | A name for this campaign |
6262
| `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) |
6464
| `config_ref` | Path to the campaign configuration file (null before setup) |
6565

6666
The orchestrator writes this atomically (temp file + rename) so a crash never leaves a corrupt checkpoint.
6767

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+
6870
## 2. ledger.json — "What happened in each iteration?"
6971

7072
**Schema:** `schemas/ledger.schema.json`

orchestrator/campaign_index.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,12 @@ def _summarize(root: Path) -> CampaignSummary | None:
114114
repo: str | None = None
115115
if root.parent.name == ".nous":
116116
repo = str(root.parent.parent.resolve())
117+
# #236: read via helper so legacy ``phase`` keys still resolve.
118+
from orchestrator.engine import read_phase_field
117119
return CampaignSummary(
118120
run_id=state.get("run_id", root.name),
119121
path=str(root.resolve()),
120-
phase=state.get("phase", "UNKNOWN"),
122+
phase=read_phase_field(state, default="UNKNOWN"),
121123
iteration=int(state.get("iteration", 0) or 0),
122124
completed_iterations=completed,
123125
active_principles=active,

orchestrator/cli.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,12 @@ def _cmd_run(args):
9292
state_path = Path(repo_path) / ".nous" / run_id / "state.json"
9393
if state_path.exists():
9494
state = json.loads(state_path.read_text())
95-
if state.get("phase") != "INIT":
95+
# #236: read via helper so legacy ``phase`` keys still resolve.
96+
from orchestrator.engine import read_phase_field
97+
phase = read_phase_field(state)
98+
if phase != "INIT":
9699
print(
97-
f"Run '{run_id}' already in progress (phase={state['phase']}). "
100+
f"Run '{run_id}' already in progress (phase={phase}). "
98101
f"Use 'nous resume' to continue.",
99102
file=sys.stderr,
100103
)

orchestrator/engine.py

Lines changed: 84 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,71 @@
22
33
Owns phase transitions and state.json checkpoint/resume.
44
This is NOT an LLM — it is a deterministic script.
5+
6+
state.json semantics
7+
--------------------
8+
9+
The on-disk ``last_entered_phase`` field reflects the **last phase the
10+
engine entered**, not the **currently active phase**. The two are not
11+
the same — artifact writes within a phase do not trigger state.json
12+
updates, so during a long phase you'll see the entry-time value linger
13+
even though the phase has been working for many seconds. After the
14+
phase finishes its work, the value continues to linger until the next
15+
``transition()`` is called and the new phase is entered.
16+
17+
This was renamed from ``phase`` in #236; the legacy key is read for
18+
backward compat (in-flight runs from older versions) and migrated to
19+
the new key on the next ``transition()`` or ``force_phase()``.
20+
21+
If you need a more aggressive progress signal (e.g. for a status
22+
dashboard polling at sub-second granularity), watch the iteration
23+
artifact directory's mtimes rather than ``state.json``.
524
"""
625
import json
726
import logging
27+
from collections.abc import Mapping
828
from datetime import datetime, timezone
929
from enum import Enum
1030
from pathlib import Path
1131
from types import MappingProxyType
32+
from typing import Any, overload
1233

1334
from orchestrator.util import atomic_write
1435

1536
logger = logging.getLogger(__name__)
1637

17-
_REQUIRED_STATE_KEYS = {"phase", "iteration", "run_id", "family", "timestamp"}
38+
# #236: the canonical on-disk key is ``last_entered_phase``. The legacy
39+
# key ``phase`` is accepted on load for backward compat (in-flight runs)
40+
# and migrated to the new key on the next save. Required-keys validation
41+
# uses the canonical name; legacy state.json files are normalized in
42+
# memory before the check runs.
43+
_PHASE_KEY = "last_entered_phase"
44+
_LEGACY_PHASE_KEY = "phase"
45+
_REQUIRED_STATE_KEYS = {_PHASE_KEY, "iteration", "run_id", "family", "timestamp"}
46+
47+
48+
@overload
49+
def read_phase_field(state: Mapping[str, Any]) -> str | None: ...
50+
@overload
51+
def read_phase_field(state: Mapping[str, Any], default: str) -> str: ...
52+
def read_phase_field(
53+
state: Mapping[str, Any],
54+
default: str | None = None,
55+
) -> str | None:
56+
"""Return state.json's ``last_entered_phase``, falling back to the
57+
legacy ``phase`` key (#236).
58+
59+
Use this in any code that reads state.json **without** going
60+
through ``Engine`` — e.g., status dashboards, the campaign index,
61+
warm-start probes. Code that holds an ``Engine`` should read
62+
``engine.last_entered_phase`` (or its alias ``engine.phase``)
63+
instead.
64+
"""
65+
if _PHASE_KEY in state:
66+
return state[_PHASE_KEY]
67+
if _LEGACY_PHASE_KEY in state:
68+
return state[_LEGACY_PHASE_KEY]
69+
return default
1870

1971

2072
class Phase(str, Enum):
@@ -73,9 +125,24 @@ def state(self) -> dict:
73125
"""Shallow copy of the current state (safe: state is always a flat dict)."""
74126
return dict(self._state)
75127

128+
@property
129+
def last_entered_phase(self) -> str:
130+
"""The last phase the engine entered.
131+
132+
NOT necessarily the currently active phase — see the module
133+
docstring for the entry-only semantics caveat (#236).
134+
"""
135+
return self._state[_PHASE_KEY]
136+
76137
@property
77138
def phase(self) -> str:
78-
return self._state["phase"]
139+
"""Alias for :pyattr:`last_entered_phase` (#236).
140+
141+
Kept for source compatibility — most callers in the orchestrator
142+
already use ``engine.phase``. New code should prefer
143+
``engine.last_entered_phase`` for clarity.
144+
"""
145+
return self._state[_PHASE_KEY]
79146

80147
@property
81148
def iteration(self) -> int:
@@ -95,13 +162,19 @@ def _load_state(self) -> dict:
95162
f"Corrupt state.json at {self.state_path}: {e}. "
96163
f"Restore from backup or re-initialize from templates/state.json."
97164
) from e
165+
# #236 migration: legacy state.json files use ``phase``; rename
166+
# in-memory before the required-keys check so validation reports
167+
# the canonical key in error messages. The next save writes the
168+
# canonical name, dropping the legacy key.
169+
if _PHASE_KEY not in state and _LEGACY_PHASE_KEY in state:
170+
state[_PHASE_KEY] = state.pop(_LEGACY_PHASE_KEY)
98171
missing = _REQUIRED_STATE_KEYS - state.keys()
99172
if missing:
100173
raise ValueError(f"state.json missing required keys: {missing}")
101174
# Validate phase is a recognized state
102-
if state["phase"] not in ALL_STATES:
175+
if state[_PHASE_KEY] not in ALL_STATES:
103176
raise ValueError(
104-
f"state.json has unrecognized phase '{state['phase']}'. "
177+
f"state.json has unrecognized phase '{state[_PHASE_KEY]}'. "
105178
f"Valid phases: {sorted(s.value for s in Phase)}"
106179
)
107180
return state
@@ -113,7 +186,7 @@ def transition(self, to_state: str) -> None:
113186
f"'{to_state}' is not a recognized phase. "
114187
f"Valid phases: {sorted(s.value for s in Phase)}"
115188
)
116-
current = self._state["phase"]
189+
current = self._state[_PHASE_KEY]
117190
if current not in TRANSITIONS:
118191
raise ValueError(f"Unknown state: {current}")
119192
if to_state not in TRANSITIONS[current]:
@@ -132,8 +205,11 @@ def transition(self, to_state: str) -> None:
132205
new_state["iteration"] += 1
133206
elif current == "DONE" and to_state == "DESIGN":
134207
new_state["iteration"] += 1
135-
new_state["phase"] = to_state
208+
new_state[_PHASE_KEY] = to_state
136209
new_state["timestamp"] = datetime.now(timezone.utc).isoformat()
210+
# #236: drop the legacy key on save so a migrated state.json
211+
# doesn't carry both names.
212+
new_state.pop(_LEGACY_PHASE_KEY, None)
137213
self._save_state(new_state)
138214
self._state = new_state
139215
logger.info("Transition: %s -> %s (iteration=%d)", current, to_state, new_state["iteration"])
@@ -151,8 +227,9 @@ def force_phase(self, phase: str) -> None:
151227
)
152228
new_state = dict(self._state)
153229
new_state["iteration"] += 1
154-
new_state["phase"] = phase
230+
new_state[_PHASE_KEY] = phase
155231
new_state["timestamp"] = datetime.now(timezone.utc).isoformat()
232+
new_state.pop(_LEGACY_PHASE_KEY, None)
156233
self._save_state(new_state)
157234
self._state = new_state
158235
logger.info("Force phase: -> %s (iteration=%d)", phase, new_state["iteration"])

orchestrator/schemas/state.schema.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@
22
"$schema": "https://json-schema.org/draft/2020-12/schema",
33
"$id": "https://github.com/AI-native-Systems-Research/agentic-strategy-evolution/schemas/state.schema.json",
44
"title": "Investigation State",
5-
"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).",
66
"type": "object",
7-
"required": ["phase", "iteration", "run_id", "family", "timestamp"],
7+
"required": ["last_entered_phase", "iteration", "run_id", "family", "timestamp"],
88
"additionalProperties": false,
99
"properties": {
10-
"phase": {
10+
"last_entered_phase": {
1111
"type": "string",
1212
"enum": [
1313
"INIT", "PRE_WORK", "DESIGN", "CRITIC", "HUMAN_DESIGN_GATE",
1414
"EXECUTE_ANALYZE", "HUMAN_FINDINGS_GATE", "DONE"
1515
],
16-
"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."
1717
},
1818
"iteration": {
1919
"type": "integer",

orchestrator/status.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,10 @@ def read_status_snapshot(
169169

170170
state = _read_json(work_dir / "state.json")
171171
if isinstance(state, dict):
172+
# #236: read via helper so legacy ``phase`` keys still resolve.
173+
from orchestrator.engine import read_phase_field
172174
snap.run_id = str(state.get("run_id", "?"))
173-
snap.phase = str(state.get("phase", "?"))
175+
snap.phase = str(read_phase_field(state, default="?"))
174176
snap.iteration = int(state.get("iteration", 0) or 0)
175177
snap.raw = state
176178

orchestrator/templates/state.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"phase": "INIT",
2+
"last_entered_phase": "INIT",
33
"iteration": 0,
44
"run_id": "TODO-SET-RUN-ID",
55
"family": null,

orchestrator/warm_start.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,14 @@ def warm_start_from_prior(
143143
prior_dir = _find_prior_dir(prior_run_id, prior_search_paths)
144144
state = _load_state(prior_dir)
145145

146-
if state.get("phase") != "DONE":
146+
# #236: read via helper so legacy ``phase`` keys still work for
147+
# in-flight campaigns being warm-started off.
148+
from orchestrator.engine import read_phase_field
149+
prior_phase = read_phase_field(state)
150+
if prior_phase != "DONE":
147151
raise RuntimeError(
148152
f"prior campaign {prior_run_id!r} is not complete "
149-
f"(phase={state.get('phase')!r}); only DONE campaigns can "
153+
f"(phase={prior_phase!r}); only DONE campaigns can "
150154
f"be warm-started from",
151155
)
152156

tests/test_campaign.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ def test_mid_flight_design_resumes_at_correct_iteration(self, tmp_path):
213213
from orchestrator.campaign import _resume_completed_campaign
214214
work_dir = _setup_work_dir(tmp_path)
215215
state = json.loads((work_dir / "state.json").read_text())
216-
state["phase"] = "DESIGN"
216+
state["last_entered_phase"] = "DESIGN"
217217
state["iteration"] = 16
218218
(work_dir / "state.json").write_text(json.dumps(state))
219219

@@ -228,7 +228,7 @@ def test_mid_flight_execute_analyze_resumes_at_correct_iteration(self, tmp_path)
228228
from orchestrator.campaign import _resume_completed_campaign
229229
work_dir = _setup_work_dir(tmp_path)
230230
state = json.loads((work_dir / "state.json").read_text())
231-
state["phase"] = "EXECUTE_ANALYZE"
231+
state["last_entered_phase"] = "EXECUTE_ANALYZE"
232232
state["iteration"] = 5
233233
(work_dir / "state.json").write_text(json.dumps(state))
234234

@@ -243,7 +243,7 @@ def test_mid_flight_iteration_1_boundary(self, tmp_path):
243243
from orchestrator.campaign import _resume_completed_campaign
244244
work_dir = _setup_work_dir(tmp_path)
245245
state = json.loads((work_dir / "state.json").read_text())
246-
state["phase"] = "DESIGN"
246+
state["last_entered_phase"] = "DESIGN"
247247
state["iteration"] = 1
248248
(work_dir / "state.json").write_text(json.dumps(state))
249249

@@ -265,7 +265,7 @@ def test_mid_flight_corrupt_iteration_falls_back_to_1(self, tmp_path, caplog):
265265
from orchestrator.campaign import _resume_completed_campaign
266266
work_dir = _setup_work_dir(tmp_path)
267267
state = json.loads((work_dir / "state.json").read_text())
268-
state["phase"] = "DESIGN"
268+
state["last_entered_phase"] = "DESIGN"
269269
state["iteration"] = 0
270270
(work_dir / "state.json").write_text(json.dumps(state))
271271

@@ -287,7 +287,7 @@ def test_mid_flight_exceeds_max_iterations_warns(self, tmp_path, caplog):
287287
from orchestrator.campaign import _resume_completed_campaign
288288
work_dir = _setup_work_dir(tmp_path)
289289
state = json.loads((work_dir / "state.json").read_text())
290-
state["phase"] = "DESIGN"
290+
state["last_entered_phase"] = "DESIGN"
291291
state["iteration"] = 16
292292
(work_dir / "state.json").write_text(json.dumps(state))
293293

@@ -304,7 +304,7 @@ def test_done_with_more_iterations_configured_resumes(self, tmp_path):
304304

305305
# Simulate a completed single-iteration campaign.
306306
state = json.loads((work_dir / "state.json").read_text())
307-
state["phase"] = "DONE"
307+
state["last_entered_phase"] = "DONE"
308308
(work_dir / "state.json").write_text(json.dumps(state))
309309
ledger = {"iterations": [
310310
{"iteration": 0, "family": "baseline"},
@@ -320,7 +320,7 @@ def test_done_at_max_iterations_does_not_resume(self, tmp_path):
320320
from orchestrator.campaign import _resume_completed_campaign
321321
work_dir = _setup_work_dir(tmp_path)
322322
state = json.loads((work_dir / "state.json").read_text())
323-
state["phase"] = "DONE"
323+
state["last_entered_phase"] = "DONE"
324324
(work_dir / "state.json").write_text(json.dumps(state))
325325
ledger = {"iterations": [
326326
{"iteration": 0, "family": "baseline"},
@@ -338,7 +338,7 @@ def test_done_but_no_real_iterations_does_not_resume(self, tmp_path):
338338
from orchestrator.campaign import _resume_completed_campaign
339339
work_dir = _setup_work_dir(tmp_path)
340340
state = json.loads((work_dir / "state.json").read_text())
341-
state["phase"] = "DONE"
341+
state["last_entered_phase"] = "DONE"
342342
(work_dir / "state.json").write_text(json.dumps(state))
343343
ledger = {"iterations": [{"iteration": 0, "family": "baseline"}]}
344344
(work_dir / "ledger.json").write_text(json.dumps(ledger))
@@ -352,7 +352,7 @@ def test_corrupt_ledger_does_not_crash_resume(self, tmp_path, caplog):
352352
from orchestrator.campaign import _resume_completed_campaign
353353
work_dir = _setup_work_dir(tmp_path)
354354
state = json.loads((work_dir / "state.json").read_text())
355-
state["phase"] = "DONE"
355+
state["last_entered_phase"] = "DONE"
356356
(work_dir / "state.json").write_text(json.dumps(state))
357357
(work_dir / "ledger.json").write_text("{this is not valid json")
358358

@@ -366,7 +366,7 @@ def test_ledger_with_malformed_rows_does_not_crash_resume(self, tmp_path):
366366
from orchestrator.campaign import _resume_completed_campaign
367367
work_dir = _setup_work_dir(tmp_path)
368368
state = json.loads((work_dir / "state.json").read_text())
369-
state["phase"] = "DONE"
369+
state["last_entered_phase"] = "DONE"
370370
(work_dir / "state.json").write_text(json.dumps(state))
371371
ledger = {"iterations": [
372372
{"iteration": 0, "family": "baseline"},

0 commit comments

Comments
 (0)