repo-manager/docs/observation-command-contracts_v0.1.md

398 lines
14 KiB
Markdown
Raw Normal View History

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