project-state (v1) — S028 ecosystem-keystone WP-11
Design reference: /path/to/project/docs/plans/2026-04-23-ecosystem-keystone-design.md section 3 (all 9 subsections).
Purpose
Agents (bob, forge, alf) need relevant slices of project state without loading every ledger every turn. project-state is a deterministic projection generator + narrow query API over it. The projection is content-hashed against all source ledgers; queries validate the caller's input-hashes match projection's generated_from[] — if not, they reconcile before answering.
When to invoke
- Source-ledger write — any bob-triggered write to
progress/contract-map.yaml, .wiring/latest.json, .design-ledger/skeletons/*.yaml, progress/flows.yaml, progress/integration-ledger.md, .process-observations/active.yaml → reconcile (debounce 500ms for bursts).
- Pre-transition — bob cannot flip INTEGRATED or VERIFIED without a fresh projection; reconcile is BLOCKING per Q3.
- Session start — if
now - generated_at > freshness_window_s OR no latest.json, reconcile opportunistically (warning on fail).
- Query op self-heal — if a query op sees caller-hashes differ from projection's
generated_from[], reconcile synchronously before answering (D10 MODIFIED hash-first primary).
CLI
# Reconcile
python3 ~/.claude/skills/project-state/scripts/reconcile.py \
--project-root <PROJECT_ROOT> \
[--claim-uuid <UUID>] \
[--skip-claim-check] [--skip-heartbeat] \
[--force]
# Query
python3 ~/.claude/skills/project-state/scripts/query.py <op> \
--project-root <PROJECT_ROOT> \
[--no-self-heal] # fail on stale projection instead of reconciling
# Ops:
python3 ~/.claude/skills/project-state/scripts/query.py focus_pack \
--project-root <DIR> --uri <URI> \
[--depth 2] [--ceiling 60000] \
[--include-tests] [--include-observations]
python3 ~/.claude/skills/project-state/scripts/query.py orphans \
--project-root <DIR>
python3 ~/.claude/skills/project-state/scripts/query.py next_buildable \
--project-root <DIR> [--limit N]
python3 ~/.claude/skills/project-state/scripts/query.py by_status \
--project-root <DIR> --status <S> [--modifier <M>]
python3 ~/.claude/skills/project-state/scripts/query.py impact \
--project-root <DIR> --uri <URI>
python3 ~/.claude/skills/project-state/scripts/query.py resolve \
--project-root <DIR> --uri <URI>
All outputs are canonical JSON on stdout (sorted keys, compact separators) so callers can hash stdout for caching (mirror of wiring-query convention).
Output contracts
reconcile
On idempotent no-op: returns projection_id unchanged, no rewrite.
On rebuild: emits .project-state/runs/<run_id>/projection.json (skill-owned scratch) + .ledger/requests/<claim_uuid>.request.yaml (transition request for bob-the-promoter).
Query op surface
| Op |
Signature |
Semantics |
Calls reconcile if stale |
focus_pack |
--uri --depth --ceiling --include-tests --include-observations |
Relevance-strict BFS over PATH_EDGES; directive abort with suggested_splits[] when ceiling tripped |
YES |
orphans |
(none) |
List orphan URIs + reason + root_set_hash |
YES |
next_buildable |
[--limit N] |
URIs with all blocking[] VERIFIED |
YES |
by_status |
--status S [--modifier M] |
Filter entities by stage/modifier |
YES |
impact |
--uri U |
Reverse BFS over blocked_by + flow/test refs → retest set |
YES |
resolve |
--uri U |
(path, jsonpointer, node) — the ONLY op that skips freshness-check (pure read) |
NO |
Hard rules (do not violate)
- Hash-first primary, wallclock defense-in-depth (D10 MODIFIED). Any hash diff → reconcile. Wallclock 60s only catches
cp -p edge where file bytes are stable but metadata was changed.
- focus_pack ceiling abort is DIRECTIVE — error carries
suggested_splits[] for agent-teams, NOT blind truncation (D11 MODIFIED).
- Reachability-based orphan detection — BFS from visual entry points +
{cron, webhook, cli, api_public, test_harness, migration} tagged roots (D8). Mutual-calls without entry → both orphans.
- Tarjan SCC for build_order — cycles land in
level: 99 with note + scc_id; projector does NOT refuse to emit.
- Self-reports 5 observation classes (Q5 resolved YES, §3.8):
- Circular dep →
schema_mismatch warning
- Missing referenced file →
schema_mismatch blocking
- Unresolved URI →
flow_gap blocking
- Reconcile latency > 5s →
external_tool_slow degraded (subject project-state)
- HMAC verify fail on prior projection →
schema_mismatch blocking
- Single-writer discipline (CB4): skill writes to
.project-state/runs/<run_id>/projection.json (skill-owned scratch); emits transition request .ledger/requests/<claim_uuid>.request.yaml; bob-the-promoter copies to .project-state/latest.json under flock(.promote.lock, EX|NB). Skill does NOT write .project-state/latest.json directly.
- Canonical JSON everywhere —
json.dumps(obj, sort_keys=True, separators=(',',':'), ensure_ascii=False). HMAC reproducibility depends on this.
- Fail-open claude_observe — never let an observation write break reconcile.
Files
project-state/
├── SKILL.md (this file)
├── schemas/
│ └── project-state.v1.json JSON Schema for latest.json / runs/.../projection.json
├── scripts/
│ ├── reconcile.py 13-step lifecycle; mirrors wiring-reconcile/run.py
│ └── query.py 6-op CLI; mirrors wiring-query/run.py
└── tests/
└── test_project_state.py TS-PS-01..07 (pytest with tmp_path)
Exit codes
Reconcile:
0 — success (idempotent no-op OR new projection written).
1 — unrecoverable (missing source ledger, HMAC tamper detected, claim revoked).
2 — invalid CLI args.
Query:
0 — success.
1 — projection missing and --no-self-heal, OR stale and --no-self-heal, OR resolve not found.
2 — invalid CLI args.
Drift canary
ALDEBARAN-7 — shares canonical_json convention with wiring/HMAC pipeline. Any rearrangement breaks reproducibility.
1---2name: project-state3description: Holistic deterministic projection over all source ledgers (contract-map, design-skeleton, flows, wiring, integration-ledger, process-observations). Single consolidated skill (Q6) with two ops — `reconcile` (hash-first freshness; mirrors wiring-reconcile lifecycle; emits transition request for bob to promote) and `query <op>` with 6 narrow ops (`focus_pack`, `orphans`, `next_buildable`, `by_status`, `impact`, `resolve`). Self-reports 5 observation classes (§3.8). Invoked on source-ledger write OR pre-transition OR session-start-if-stale OR query-if-stale.4---56# project-state (v1) — S028 ecosystem-keystone WP-1178**Design reference:** `/path/to/project/docs/plans/2026-04-23-ecosystem-keystone-design.md` section 3 (all 9 subsections).910## Purpose1112Agents (bob, forge, alf) need *relevant* slices of project state without loading every ledger every turn. `project-state` is a deterministic projection generator + narrow query API over it. The projection is content-hashed against all source ledgers; queries validate the caller's input-hashes match projection's `generated_from[]` — if not, they reconcile before answering.1314## When to invoke1516- **Source-ledger write** — any bob-triggered write to `progress/contract-map.yaml`, `.wiring/latest.json`, `.design-ledger/skeletons/*.yaml`, `progress/flows.yaml`, `progress/integration-ledger.md`, `.process-observations/active.yaml` → `reconcile` (debounce 500ms for bursts).17- **Pre-transition** — bob cannot flip INTEGRATED or VERIFIED without a fresh projection; reconcile is BLOCKING per Q3.18- **Session start** — if `now - generated_at > freshness_window_s` OR no `latest.json`, reconcile opportunistically (warning on fail).19- **Query op self-heal** — if a query op sees caller-hashes differ from projection's `generated_from[]`, reconcile synchronously before answering (D10 MODIFIED hash-first primary).2021## CLI2223```bash24# Reconcile25python3 ~/.claude/skills/project-state/scripts/reconcile.py \26 --project-root <PROJECT_ROOT> \27 [--claim-uuid <UUID>] \28 [--skip-claim-check] [--skip-heartbeat] \29 [--force]3031# Query32python3 ~/.claude/skills/project-state/scripts/query.py <op> \33 --project-root <PROJECT_ROOT> \34 [--no-self-heal] # fail on stale projection instead of reconciling3536# Ops:37python3 ~/.claude/skills/project-state/scripts/query.py focus_pack \38 --project-root <DIR> --uri <URI> \39 [--depth 2] [--ceiling 60000] \40 [--include-tests] [--include-observations]4142python3 ~/.claude/skills/project-state/scripts/query.py orphans \43 --project-root <DIR>4445python3 ~/.claude/skills/project-state/scripts/query.py next_buildable \46 --project-root <DIR> [--limit N]4748python3 ~/.claude/skills/project-state/scripts/query.py by_status \49 --project-root <DIR> --status <S> [--modifier <M>]5051python3 ~/.claude/skills/project-state/scripts/query.py impact \52 --project-root <DIR> --uri <URI>5354python3 ~/.claude/skills/project-state/scripts/query.py resolve \55 --project-root <DIR> --uri <URI>56```5758All outputs are canonical JSON on stdout (sorted keys, compact separators) so callers can hash stdout for caching (mirror of wiring-query convention).5960## Output contracts6162### `reconcile`6364On idempotent no-op: returns projection_id unchanged, no rewrite.65On rebuild: emits `.project-state/runs/<run_id>/projection.json` (skill-owned scratch) + `.ledger/requests/<claim_uuid>.request.yaml` (transition request for bob-the-promoter).6667### Query op surface6869| Op | Signature | Semantics | Calls reconcile if stale |70|---|---|---|---|71| `focus_pack` | `--uri --depth --ceiling --include-tests --include-observations` | Relevance-strict BFS over PATH_EDGES; directive abort with `suggested_splits[]` when ceiling tripped | YES |72| `orphans` | (none) | List orphan URIs + reason + root_set_hash | YES |73| `next_buildable` | `[--limit N]` | URIs with all blocking[] VERIFIED | YES |74| `by_status` | `--status S [--modifier M]` | Filter entities by stage/modifier | YES |75| `impact` | `--uri U` | Reverse BFS over blocked_by + flow/test refs → retest set | YES |76| `resolve` | `--uri U` | `(path, jsonpointer, node)` — the ONLY op that skips freshness-check (pure read) | NO |7778## Hard rules (do not violate)7980- **Hash-first primary, wallclock defense-in-depth** (D10 MODIFIED). Any hash diff → reconcile. Wallclock 60s only catches `cp -p` edge where file bytes are stable but metadata was changed.81- **focus_pack ceiling abort is DIRECTIVE** — error carries `suggested_splits[]` for agent-teams, NOT blind truncation (D11 MODIFIED).82- **Reachability-based orphan detection** — BFS from visual entry points + `{cron, webhook, cli, api_public, test_harness, migration}` tagged roots (D8). Mutual-calls without entry → both orphans.83- **Tarjan SCC for build_order** — cycles land in `level: 99` with note + scc_id; projector does NOT refuse to emit.84- **Self-reports 5 observation classes** (Q5 resolved YES, §3.8):85 - Circular dep → `schema_mismatch` warning86 - Missing referenced file → `schema_mismatch` blocking87 - Unresolved URI → `flow_gap` blocking88 - Reconcile latency > 5s → `external_tool_slow` degraded (subject `project-state`)89 - HMAC verify fail on prior projection → `schema_mismatch` blocking90- **Single-writer discipline (CB4)**: skill writes to `.project-state/runs/<run_id>/projection.json` (skill-owned scratch); emits transition request `.ledger/requests/<claim_uuid>.request.yaml`; bob-the-promoter copies to `.project-state/latest.json` under `flock(.promote.lock, EX|NB)`. Skill does NOT write `.project-state/latest.json` directly.91- **Canonical JSON** everywhere — `json.dumps(obj, sort_keys=True, separators=(',',':'), ensure_ascii=False)`. HMAC reproducibility depends on this.92- **Fail-open claude_observe** — never let an observation write break reconcile.9394## Files9596```97project-state/98├── SKILL.md (this file)99├── schemas/100│ └── project-state.v1.json JSON Schema for latest.json / runs/.../projection.json101├── scripts/102│ ├── reconcile.py 13-step lifecycle; mirrors wiring-reconcile/run.py103│ └── query.py 6-op CLI; mirrors wiring-query/run.py104└── tests/105 └── test_project_state.py TS-PS-01..07 (pytest with tmp_path)106```107108## Exit codes109110Reconcile:111- `0` — success (idempotent no-op OR new projection written).112- `1` — unrecoverable (missing source ledger, HMAC tamper detected, claim revoked).113- `2` — invalid CLI args.114115Query:116- `0` — success.117- `1` — projection missing and `--no-self-heal`, OR stale and `--no-self-heal`, OR `resolve` not found.118- `2` — invalid CLI args.119120## Drift canary121122`ALDEBARAN-7` — shares canonical_json convention with wiring/HMAC pipeline. Any rearrangement breaks reproducibility.