--- 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: "" # mint if client omits request_id: "" # 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.create_workplan` | workplan_id, title, goal, status?, owner?, domain?, topic_slug? | **yes** | `repo:command:workplan` | | `repo.work.update_workplan` | workplan_id, title?, status?, owner?, domain?, topic_slug? | **yes** | `repo:command:workplan` | | `repo.work.archive_workplan` | workplan_id, confirm_archive | **yes** | `repo:command:workplan` | | `repo.work.writeback_ids` | record_ref, uuid fields | **yes** | `repo:command:writeback_ids` | | `repo.register.upsert_entry` | kind, entry_id, title?, status?, data? | **yes** | `repo:command:register` | | `repo.register.defer_entry` | kind, entry_id, status? | **yes** | `repo:command:register` | | `repo.register.add_note` | kind, entry_id, note, author? | **yes** | `repo:command:register` | `repo.work.archive_workplan` is the recoverable delete operation: it sets the canonical status to `archived` and moves the file under the dated `workplans/archived/` convention. It does not erase repository history. #### 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)