docs(RMGR-WP-0001): complete T02 observation and command contracts
Define helixforge.repo-manager 0.1: observation ops, governed command catalog, authz scopes, idempotency, evidence, failures, and hub-core ports.
This commit is contained in:
parent
ca65ac9cf3
commit
1437999eb7
4 changed files with 543 additions and 1 deletions
387
docs/observation-command-contracts_v0.1.md
Normal file
387
docs/observation-command-contracts_v0.1.md
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
---
|
||||
id: RMGR-ARCH-CMD-0001
|
||||
type: architecture
|
||||
title: "Observation and command contracts v0.1"
|
||||
status: draft-reviewable
|
||||
version: "0.1"
|
||||
created: "2026-08-09"
|
||||
updated: "2026-08-09"
|
||||
workplan_task: RMGR-WP-0001-T02
|
||||
depends_on:
|
||||
- RMGR-ARCH-REP-0001
|
||||
related:
|
||||
- docs/repository-representation_v0.1.md
|
||||
- docs/observation-command-contracts_v0.1.yaml
|
||||
- INTENT.md
|
||||
- prj-state-hub-retirement/architecture/hub-extension-architecture_v0.1.md
|
||||
- prj-state-hub-retirement/architecture/information-model_v0.1.md
|
||||
---
|
||||
|
||||
# Observation and command contracts v0.1
|
||||
|
||||
## Purpose
|
||||
|
||||
Define **versioned, persistence-free** contracts for:
|
||||
|
||||
1. **Observation** — normalized repository facts and work-record index reads
|
||||
2. **Commands** — governed mutations with authz, idempotency, correlation, and
|
||||
Git-backed evidence
|
||||
3. **Hub-core integration** — how `port.repo` and `port.work` expose these
|
||||
without leaking SQLAlchemy models
|
||||
|
||||
Companion: [`observation-command-contracts_v0.1.yaml`](observation-command-contracts_v0.1.yaml).
|
||||
|
||||
Implementation (OpenAPI generation, runtime) follows T04 foundation decision;
|
||||
this document freezes semantics for T03 extraction and T05 E2E proof.
|
||||
|
||||
---
|
||||
|
||||
## Contract identity
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| `contract_id` | `helixforge.repo-manager` |
|
||||
| `contract_version` | `0.1.0` (draft-reviewable) |
|
||||
| `representation_ref` | `RMGR-ARCH-REP-0001` |
|
||||
| Compatibility | Additive in minor; breaking requires major + dual-run |
|
||||
|
||||
**Hard rule:** Request/response DTOs are the public surface. Persistence
|
||||
schemas, table names, and ORM classes are private to Repo Manager.
|
||||
|
||||
---
|
||||
|
||||
## Common envelope
|
||||
|
||||
Every API message (HTTP/JSON, event payload, or MCP tool result) SHOULD carry:
|
||||
|
||||
```yaml
|
||||
api_version: "0.1"
|
||||
correlation_id: "<UUIDv7>" # mint if client omits
|
||||
request_id: "<UUIDv7>" # per call
|
||||
actor:
|
||||
type: human | agent | service
|
||||
id: "agt-…" | "user-…" | "svc-…"
|
||||
authz:
|
||||
subject: "…" # identity principal
|
||||
scopes: ["repo:read", "repo:command:…"]
|
||||
decision: allow | deny # on response if evaluated
|
||||
```
|
||||
|
||||
### Correlation
|
||||
|
||||
- One **human/agent action** that spans observe → command → event uses one
|
||||
`correlation_id`.
|
||||
- Hub-core progress/interaction events that describe the same action reuse it
|
||||
(IA dual-write ban still applies: one semantic fact, correlated kinds).
|
||||
|
||||
### Idempotency
|
||||
|
||||
| Surface | Key | Behavior |
|
||||
| --- | --- | --- |
|
||||
| Commands | `Idempotency-Key` header or `idempotency_key` body (UUIDv7) | Same key + same command type + same payload hash → return original result; conflict if payload differs |
|
||||
| Observation | none required | Safe to retry; may return fresher `observed_at` |
|
||||
| Writeback | key includes `repo_uuid` + `source_path` + content hash | Replay does not double-commit if evidence SHA already recorded |
|
||||
|
||||
Retention of idempotency records: ≥ 24h (operational policy may extend).
|
||||
|
||||
### Evidence object
|
||||
|
||||
Returned on every **completed** command (success or handled failure after
|
||||
partial work):
|
||||
|
||||
```yaml
|
||||
evidence:
|
||||
status: accepted | applied | rejected | failed
|
||||
git_sha: "…" | null # primary proof when applied
|
||||
git_refs: ["refs/heads/…"] # optional
|
||||
forge_pr_url: null | "…"
|
||||
files_touched: ["path", …]
|
||||
content_hashes: { "path": "sha256:…" }
|
||||
observed_at: "ISO-8601"
|
||||
notes: "non-secret"
|
||||
```
|
||||
|
||||
**Rule:** `status: applied` without `git_sha` is invalid for file-mutating
|
||||
commands (except pure operator-config commands that only change host_paths).
|
||||
|
||||
### Failure model
|
||||
|
||||
| Code | HTTP-ish | Meaning | Client action |
|
||||
| --- | --- | --- | --- |
|
||||
| `unauthorized` | 401/403 | Authn/authz failed | Fix credentials/scopes |
|
||||
| `not_found` | 404 | Unknown repo or record | Re-resolve slug/uuid |
|
||||
| `conflict` | 409 | Drift, lock, or idempotency payload mismatch | Re-observe; resolve drift |
|
||||
| `precondition_failed` | 412 | Stale `expected_head_sha` / base revision | Refresh HEAD; retry |
|
||||
| `validation_error` | 422 | Schema/vocab/command args invalid | Fix request |
|
||||
| `policy_denied` | 403 | Policy port deny | Escalate / change policy |
|
||||
| `unavailable` | 503 | Forge/git/policy/hub down | Retry with backoff |
|
||||
| `internal` | 500 | Unexpected | Operator; attach correlation_id |
|
||||
|
||||
Failures are **structured JSON**, never raw stack traces to external clients.
|
||||
|
||||
```yaml
|
||||
error:
|
||||
code: conflict
|
||||
message: "Index drift on workplans/RMGR-WP-0001-foundation.md"
|
||||
correlation_id: "…"
|
||||
details:
|
||||
finding_ids: ["…"]
|
||||
retryable: false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Authorization context
|
||||
|
||||
Commands and sensitive observation paths require:
|
||||
|
||||
| Field | Source |
|
||||
| --- | --- |
|
||||
| `subject` | Identity authority / hub-core addressing |
|
||||
| `scopes` | Policy (examples below) |
|
||||
| `repo_uuid` or `repo_slug` | Resource |
|
||||
| `lane` | Optional autonomy lane for agent actors |
|
||||
| `reason` | Required for high-risk commands |
|
||||
|
||||
### Scope vocabulary (v0.1)
|
||||
|
||||
| Scope | Allows |
|
||||
| --- | --- |
|
||||
| `repo:read` | Observation of representation + index |
|
||||
| `repo:register` | Create/update registration & host_paths |
|
||||
| `repo:reconcile` | Run consistency / rebuild projections |
|
||||
| `repo:command:work_status` | File-backed status writeback (task/WP) |
|
||||
| `repo:command:writeback_ids` | UUID / hub id frontmatter writeback |
|
||||
| `repo:command:lifecycle` | lifecycle transitions (archive, …) |
|
||||
| `repo:command:agent_assign` | coach/lead/director assignment |
|
||||
| `repo:admin` | Break-glass operator |
|
||||
|
||||
**Policy port:** Repo Manager **evaluates** via hub-core `port.policy` (or
|
||||
local allowlist in dev). It does not become the authorization authority.
|
||||
|
||||
Green-lane agents may hold only `repo:read` + narrow command scopes per
|
||||
autonomy policy; non-green requires explicit human/policy allow.
|
||||
|
||||
---
|
||||
|
||||
## Observation contract (`port.repo` / `port.work` reads)
|
||||
|
||||
### Normalized fact types
|
||||
|
||||
| Fact | Description | Source of truth |
|
||||
| --- | --- | --- |
|
||||
| `RepositorySnapshot` | ManagedRepository DTO (REP-0001) | files + operator + git observation |
|
||||
| `WorkRecordIndexEntry` | spine fields for one work record | file + index |
|
||||
| `ConsistencyFinding` | open C-rule style finding | derived |
|
||||
| `RevisionObservation` | head_sha, dirty, observed_at | git |
|
||||
| `AgentAssignment` | coach/lead/director | file/registry projection |
|
||||
| `SignalSet` | named derived signals | derived |
|
||||
|
||||
### Read operations
|
||||
|
||||
| Op | Port | Request | Response |
|
||||
| --- | --- | --- | --- |
|
||||
| `GetRepository` | repo | `slug` or `uuid` | `RepositorySnapshot` |
|
||||
| `ListRepositories` | repo | filters: domain, category, lifecycle, tag | page of snapshots |
|
||||
| `ObserveRevision` | repo | `slug`, optional `host_id` | `RevisionObservation` |
|
||||
| `ListWorkRecords` | work | `repo`, kind?, status? | page of index entries |
|
||||
| `GetWorkRecord` | work | `uuid` or `(repo, canonical_id)` | entry + `source_path` |
|
||||
| `ListFindings` | repo | `repo`, open_only? | findings |
|
||||
| `GetSignals` | repo | `repo` | `SignalSet` |
|
||||
|
||||
Pagination: cursor or limit/offset with stable sort (`slug` / `updated_at`).
|
||||
No ORM objects in responses.
|
||||
|
||||
### Observation guarantees
|
||||
|
||||
- **Read-your-writes:** After `applied` command with `git_sha`, a subsequent
|
||||
`GetRepository`/`GetWorkRecord` within the same process generation MUST
|
||||
reflect that revision or return `observed_at` older with explicit
|
||||
`stale: true` and `head_sha` for client retry.
|
||||
- **Rebuild:** Full reindex from files produces equivalent index content
|
||||
(modulo mint of new UUIDs only for records never writeback-assigned).
|
||||
|
||||
---
|
||||
|
||||
## Command contract (governed mutations)
|
||||
|
||||
### Command lifecycle
|
||||
|
||||
```text
|
||||
accepted → (running) → applied | rejected | failed
|
||||
```
|
||||
|
||||
| State | Meaning |
|
||||
| --- | --- |
|
||||
| `accepted` | Validated, authz allow, idempotency recorded; not yet on Git |
|
||||
| `running` | Optional intermediate for long ops |
|
||||
| `applied` | Evidence includes git_sha (or config-only evidence) |
|
||||
| `rejected` | Policy/validation; no mutation |
|
||||
| `failed` | Started but did not complete cleanly; may need repair |
|
||||
|
||||
Clients MUST treat **only `applied`** as success for dependent steps.
|
||||
|
||||
### Command envelope
|
||||
|
||||
```yaml
|
||||
command:
|
||||
type: "repo.work.update_task_status" # cataloged
|
||||
api_version: "0.1"
|
||||
idempotency_key: "…"
|
||||
correlation_id: "…"
|
||||
repo: { slug: "repo-manager" } # or uuid
|
||||
expected_head_sha: "…" | null # optimistic concurrency
|
||||
actor: { type: agent, id: "…" }
|
||||
reason: "session checkpoint"
|
||||
params: { … } # type-specific
|
||||
```
|
||||
|
||||
### Command catalog (v0.1)
|
||||
|
||||
#### Registration & operator config
|
||||
|
||||
| Type | Params | Mutates Git? | Scope |
|
||||
| --- | --- | --- | --- |
|
||||
| `repo.register` | slug, remote_url?, classification?, description? | no* | `repo:register` |
|
||||
| `repo.set_host_path` | host_id, path | no | `repo:register` |
|
||||
| `repo.set_lifecycle` | lifecycle, reason | no** | `repo:command:lifecycle` |
|
||||
|
||||
\* May write registration notes only if a repo file policy says so (default: no).
|
||||
\*\* Lifecycle is projection+operator state; forge archive is separate/explicit.
|
||||
|
||||
#### Observation-triggered
|
||||
|
||||
| Type | Params | Mutates Git? | Scope |
|
||||
| --- | --- | --- | --- |
|
||||
| `repo.reconcile` | full? paths? | no (index only) | `repo:reconcile` |
|
||||
| `repo.rebuild_index` | kinds? | no | `repo:reconcile` |
|
||||
|
||||
#### File-backed work (writeback)
|
||||
|
||||
| Type | Params | Mutates Git? | Scope |
|
||||
| --- | --- | --- | --- |
|
||||
| `repo.work.update_task_status` | task_id, status, blocking_reason? | **yes** | `repo:command:work_status` |
|
||||
| `repo.work.update_workplan_status` | workplan_id, status | **yes** | `repo:command:work_status` |
|
||||
| `repo.work.writeback_ids` | record_ref, uuid fields | **yes** | `repo:command:writeback_ids` |
|
||||
|
||||
#### Agents
|
||||
|
||||
| Type | Params | Mutates Git? | Scope |
|
||||
| --- | --- | --- | --- |
|
||||
| `repo.agents.assign` | coach?, lead?, director? | prefer yes (file) | `repo:command:agent_assign` |
|
||||
|
||||
### Command execution rules
|
||||
|
||||
1. **Authz first** — deny before any file lock.
|
||||
2. **Load observation** — if `expected_head_sha` set and differs → `precondition_failed`.
|
||||
3. **Drift gate** — mutating commands on a path with open high-severity finding
|
||||
→ `conflict` unless `force: true` + `repo:admin`.
|
||||
4. **Apply** — single atomic Git commit per command preferred; message includes
|
||||
`correlation_id` and command type.
|
||||
5. **Reindex** — update projections for touched paths.
|
||||
6. **Emit** — repository change event (IA: `repository_change_event`) with
|
||||
correlation_id; hub-core may project further.
|
||||
7. **Never** create workplans/tasks from telemetry or messages alone.
|
||||
|
||||
### What is not a Repo Manager command
|
||||
|
||||
| Action | Owner |
|
||||
| --- | --- |
|
||||
| Send agent message / inbox | hub-core `port.messaging` |
|
||||
| Append progress event | hub-core `port.events.progress` |
|
||||
| Schedule cron / ops run | activity-core `port.schedule` |
|
||||
| Merge PR on forge without Git in checkout | forge + human policy (optional later adapter) |
|
||||
| Policy decision | policy service |
|
||||
|
||||
---
|
||||
|
||||
## Events emitted
|
||||
|
||||
| Event type | When | Payload (non-secret) |
|
||||
| --- | --- | --- |
|
||||
| `repo.registered` | register | slug, uuid, domain |
|
||||
| `repo.observed` | optional periodic | slug, head_sha |
|
||||
| `repo.reconciled` | reconcile done | finding counts |
|
||||
| `repo.command.applied` | command success | type, git_sha, files |
|
||||
| `repo.command.failed` | command fail | type, code |
|
||||
| `repo.work.indexed` | new/updated index entry | kind, id, path |
|
||||
| `repo.drift.detected` | new finding | finding summary |
|
||||
|
||||
Consumers: hub-core projections, activity-core triggers. Payloads MUST NOT
|
||||
include secrets, raw tokens, or full file contents by default (hashes + paths
|
||||
only).
|
||||
|
||||
---
|
||||
|
||||
## Hub-core integration
|
||||
|
||||
```text
|
||||
agents / domain hubs / MCP
|
||||
│
|
||||
▼
|
||||
hub-core
|
||||
port.repo │ port.work
|
||||
│
|
||||
▼
|
||||
repo-manager
|
||||
(this contract)
|
||||
│
|
||||
▼
|
||||
Git checkouts + files
|
||||
```
|
||||
|
||||
| Hub-core need | Repo Manager op |
|
||||
| --- | --- |
|
||||
| Address a repository | `GetRepository` |
|
||||
| Orient on repo work | `ListWorkRecords` + signals |
|
||||
| Apply agent task status | `repo.work.update_task_status` |
|
||||
| Consistency automation | `repo.reconcile` (often via activity-core) |
|
||||
| Domain summary (partial) | snapshots + work counts |
|
||||
|
||||
Hub-core **must not**:
|
||||
|
||||
- open Repo Manager DB connections;
|
||||
- import `repo_manager.models`;
|
||||
- dual-write work status only to its own DB without command evidence.
|
||||
|
||||
During State Hub dual-run, a **compatibility adapter** may implement the same
|
||||
command types by calling State Hub APIs, then flip to native RM (T03/T05).
|
||||
|
||||
---
|
||||
|
||||
## MCP / CLI mapping (illustrative)
|
||||
|
||||
| User intent | Contract |
|
||||
| --- | --- |
|
||||
| `statehub fix-consistency` (today) | → `repo.reconcile` (+ legacy adapter) |
|
||||
| `update_task_status` MCP | → `repo.work.update_task_status` when cut over |
|
||||
| `register-from-classification` | → `repo.register` + classification observe |
|
||||
|
||||
Exact tool names land with runtime packaging (T04).
|
||||
|
||||
---
|
||||
|
||||
## Conformance tests (minimum for T05)
|
||||
|
||||
| # | Test |
|
||||
| --- | --- |
|
||||
| C1 | GetRepository returns DTO without ORM fields |
|
||||
| C2 | Reconcile is idempotent on clean tree |
|
||||
| C3 | update_task_status with same idempotency_key replays |
|
||||
| C4 | Stale expected_head_sha → precondition_failed |
|
||||
| C5 | Deny without scope → unauthorized/policy_denied |
|
||||
| C6 | applied response always has git_sha for file commands |
|
||||
| C7 | Rebuild index matches prior entries for UUID-written records |
|
||||
| C8 | Event payload has correlation_id and no secret keys |
|
||||
|
||||
---
|
||||
|
||||
## Acceptance (T02)
|
||||
|
||||
- [x] Normalized observation facts and read ops defined
|
||||
- [x] Command catalog with lifecycle, scopes, evidence
|
||||
- [x] Idempotency, correlation, failure model specified
|
||||
- [x] Authz context and policy port boundary stated
|
||||
- [x] Hub-core port mapping without persistence leakage
|
||||
- [x] Machine-readable companion YAML
|
||||
- [ ] Runtime OpenAPI + automated suite (T04/T05)
|
||||
Loading…
Add table
Add a link
Reference in a new issue