397 lines
14 KiB
Markdown
397 lines
14 KiB
Markdown
---
|
|
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)
|