openapi: 3.1.0
info:
  title: Managent Coordination API
  version: 0.1.0
  description: Bearer-authenticated lease coordination. Mutating create operations accept Idempotency-Key.
servers:
  - url: http://localhost:8787
security:
  - bearerAuth: []
paths:
  /healthz:
    get:
      security: []
      operationId: health
      responses:
        "200":
          description: Healthy
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok: { type: boolean, const: true }
  /v1/leases:
    post:
      operationId: acquireLease
      parameters:
        - in: header
          name: Idempotency-Key
          schema: { type: string }
          description: Reuse for retries of one logical acquire.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AcquireLeaseRequest" }
      responses:
        "201":
          description: Granted or queued
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AcquireLeaseResponse" }
        "4XX": { $ref: "#/components/responses/Error" }
  /v1/leases/{leaseId}:
    get:
      operationId: getLease
      parameters:
        - $ref: "#/components/parameters/LeaseId"
      responses:
        "200":
          description: Lease view
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LeaseView" }
        "4XX": { $ref: "#/components/responses/Error" }
  /v1/leases/{leaseId}/heartbeat:
    post:
      operationId: heartbeatLease
      parameters:
        - $ref: "#/components/parameters/LeaseId"
      responses:
        "200":
          description: Renewed lease
          content:
            application/json:
              schema: { $ref: "#/components/schemas/HeartbeatResponse" }
        "4XX": { $ref: "#/components/responses/Error" }
  /v1/leases/{leaseId}/release:
    post:
      operationId: releaseLease
      parameters:
        - $ref: "#/components/parameters/LeaseId"
        - in: header
          name: Idempotency-Key
          schema: { type: string }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason: { type: string, enum: [done, abandoned] }
      responses:
        "200":
          description: Released lease
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LeaseView" }
        "4XX": { $ref: "#/components/responses/Error" }
  /v1/leases/{leaseId}/transfer:
    post:
      operationId: transferLease
      description: >
        Minimal identity takeover (v3 §1.15). Agent B continues agent A's held
        task — same paths/status/TTL. Hotspot children follow. Audited as
        lease.transfer. See docs/ops/ORPHAN-LEASE-POLICY.md.
      parameters:
        - $ref: "#/components/parameters/LeaseId"
        - in: header
          name: Idempotency-Key
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [to_agent_instance_id]
              properties:
                to_agent_instance_id: { type: string, minLength: 1 }
      responses:
        "200":
          description: Lease view with updated agent_instance_id
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LeaseView" }
        "404": { $ref: "#/components/responses/Error" }
        "409": { $ref: "#/components/responses/Error" }
        "4XX": { $ref: "#/components/responses/Error" }
  /v1/repos/{owner}/{name}/map:
    get:
      operationId: territoryMap
      parameters:
        - { in: path, name: owner, required: true, schema: { type: string } }
        - { in: path, name: name, required: true, schema: { type: string } }
        - { in: query, name: cursor, schema: { type: string } }
        - { in: query, name: limit, schema: { type: integer, minimum: 1, maximum: 200 } }
      responses:
        "200":
          description: Active and queued territories
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TerritoryMapResponse" }
        "4XX": { $ref: "#/components/responses/Error" }
  /v1/repos/{owner}/{name}/semantic-verification:
    get:
      operationId: getSemanticVerification
      description: Separate import/typecheck/build/test evidence. Never contributes to textual conflict statistics.
      parameters:
        - { in: path, name: owner, required: true, schema: { type: string } }
        - { in: path, name: name, required: true, schema: { type: string } }
      responses:
        "200":
          description: Repository semantic configuration and recent isolated-tree runs
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SemanticVerificationResponse" }
        "4XX": { $ref: "#/components/responses/Error" }
  /v1/repos/{owner}/{name}/semantic-verification/config:
    put:
      operationId: configureSemanticVerification
      parameters:
        - { in: path, name: owner, required: true, schema: { type: string } }
        - { in: path, name: name, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SemanticVerificationConfig" }
      responses:
        "200":
          description: Validated repository configuration
        "4XX": { $ref: "#/components/responses/Error" }
  /v1/repos/{owner}/{name}/enforcement:
    get:
      operationId: getEnforcement
      description: Graduated enforcement level for the repo (advisory|warn|shadow|hard_fail). Env override surfaces as source=env_override.
      parameters:
        - { in: path, name: owner, required: true, schema: { type: string } }
        - { in: path, name: name, required: true, schema: { type: string } }
      responses:
        "200":
          description: Current enforcement policy
          content:
            application/json:
              schema: { $ref: "#/components/schemas/EnforcementResponse" }
        "4XX": { $ref: "#/components/responses/Error" }
    put:
      operationId: setEnforcement
      description: Set per-repo enforcement level. Default for new repos is advisory; never default hard_fail.
      parameters:
        - { in: path, name: owner, required: true, schema: { type: string } }
        - { in: path, name: name, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [enforcement_level]
              properties:
                enforcement_level: { $ref: "#/components/schemas/EnforcementLevel" }
      responses:
        "200":
          description: Updated enforcement policy
          content:
            application/json:
              schema: { $ref: "#/components/schemas/EnforcementResponse" }
        "4XX": { $ref: "#/components/responses/Error" }
  /v1/repos/{owner}/{name}/shadow:
    get:
      operationId: listShadowEvents
      description: Shadow observe counterfactuals (would-have-queued / would-have-collided). Measure-only; not folded into ROI.
      parameters:
        - { in: path, name: owner, required: true, schema: { type: string } }
        - { in: path, name: name, required: true, schema: { type: string } }
        - { in: query, name: cursor, schema: { type: string } }
        - { in: query, name: limit, schema: { type: integer, minimum: 1, maximum: 200 } }
      responses:
        "200":
          description: Shadow events and totals
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ShadowEventsResponse" }
        "4XX": { $ref: "#/components/responses/Error" }
  /v1/repos/{owner}/{name}/policy:
    get:
      operationId: getRepoPolicy
      description: |
        Resolved `.managent.yml` policy for the repo (v3 §7.18). Git (file in
        `local_path`) is source of truth for enterprise; workspace UI may mirror
        later. Missing file → advisory defaults.
      parameters:
        - { in: path, name: owner, required: true, schema: { type: string } }
        - { in: path, name: name, required: true, schema: { type: string } }
      responses:
        "200":
          description: Current policy snapshot
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RepoPolicyResponse" }
        "4XX": { $ref: "#/components/responses/Error" }
  /v1/repos/{owner}/{name}/policy/reload:
    post:
      operationId: reloadRepoPolicy
      description: |
        Re-read `.managent.yml` from the repo `local_path` and apply enforcement
        via `repos.enforcement_level` (same ladder; no parallel enum). Git wins.
      parameters:
        - { in: path, name: owner, required: true, schema: { type: string } }
        - { in: path, name: name, required: true, schema: { type: string } }
      responses:
        "200":
          description: Reloaded policy
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RepoPolicyReloadResponse" }
        "4XX": { $ref: "#/components/responses/Error" }
  /v1/github/webhook:
    post:
      operationId: githubWebhook
      security: []
      description: GitHub App pull_request webhook. Requires X-Hub-Signature-256 over the exact raw request bytes.
      parameters:
        - { in: header, name: X-Hub-Signature-256, required: true, schema: { type: string } }
        - { in: header, name: X-GitHub-Event, required: true, schema: { type: string } }
      responses:
        "200": { description: Processed or safely ignored }
        "401": { $ref: "#/components/responses/Error" }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
  parameters:
    LeaseId:
      in: path
      name: leaseId
      required: true
      schema: { type: string }
  responses:
    Error:
      description: Standard error envelope
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
  schemas:
    EnforcementLevel:
      type: string
      enum: [advisory, warn, shadow, hard_fail]
      description: Graduated policy ladder. New repos default to advisory; never hard_fail by default.
    EnforcementResponse:
      type: object
      required: [enforcement_level, source, default]
      properties:
        enforcement_level: { $ref: "#/components/schemas/EnforcementLevel" }
        source: { type: string, enum: [repo, env_override] }
        default: { $ref: "#/components/schemas/EnforcementLevel" }
        levels:
          type: array
          items: { $ref: "#/components/schemas/EnforcementLevel" }
        stored: { $ref: "#/components/schemas/EnforcementLevel" }
    RepoPolicy:
      type: object
      required:
        - enforcement
        - scope_prediction
        - lease_ttl_seconds
        - allowed_clients
        - codeowners_suggestions
        - exclude_paths
        - monorepo
        - source
      properties:
        enforcement: { $ref: "#/components/schemas/EnforcementLevel" }
        scope_prediction: { type: string, enum: [on, off] }
        lease_ttl_seconds: { type: integer }
        allowed_clients:
          type: array
          items: { type: string, enum: [cli, mcp, sdk, github_actions] }
        codeowners_suggestions: { type: boolean }
        exclude_paths:
          type: array
          items: { type: string }
        monorepo:
          type: object
          properties:
            package_roots:
              type: array
              items: { type: string }
        source: { type: string, enum: [file, default] }
        path: { type: string, nullable: true }
        local_path_missing: { type: boolean }
    RepoPolicyResponse:
      type: object
      required: [policy, enforcement_level, note]
      properties:
        policy: { $ref: "#/components/schemas/RepoPolicy" }
        enforcement_level: { $ref: "#/components/schemas/EnforcementLevel" }
        note: { type: string }
    RepoPolicyReloadResponse:
      type: object
      required: [policy, applied_enforcement, enforcement_level, note]
      properties:
        policy: { $ref: "#/components/schemas/RepoPolicy" }
        applied_enforcement: { type: boolean }
        enforcement_level: { $ref: "#/components/schemas/EnforcementLevel" }
        note: { type: string }
    ShadowEventsResponse:
      type: object
      required: [events, totals, next_cursor]
      properties:
        events:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              lease_id: { type: string }
              task_id: { type: string }
              would_have_queued: { type: boolean }
              would_have_collided: { type: boolean }
              blocked_by_lease_ids: { type: array, items: { type: string } }
              paths: { type: array, items: { type: string } }
              created_at: { type: integer }
        totals:
          type: object
          properties:
            events: { type: integer }
            would_have_queued: { type: integer }
            would_have_collided: { type: integer }
        next_cursor: { type: [string, "null"] }
    SemanticCommand:
      type: object
      required: [kind, argv]
      properties:
        kind: { type: string, enum: [typecheck, build, test] }
        argv:
          type: array
          minItems: 1
          maxItems: 64
          items: { type: string }
          description: Executed directly without a shell; executable must be allowlisted.
        cwd: { type: string, default: ".", description: Relative path contained by the isolated checkout. }
        timeout_ms: { type: integer, minimum: 100, maximum: 120000, default: 30000 }
        env_allowlist:
          type: array
          maxItems: 32
          items: { type: string, pattern: "^[A-Z][A-Z0-9_]*$" }
    SemanticVerificationConfig:
      type: object
      properties:
        enabled: { type: boolean, default: false }
        mode: { type: string, enum: [imports, typescript, full], default: imports }
        commands:
          type: array
          maxItems: 8
          items: { $ref: "#/components/schemas/SemanticCommand" }
    SemanticVerificationResponse:
      type: object
      required: [config, latest_status, runs]
      properties:
        config: { $ref: "#/components/schemas/SemanticVerificationConfig" }
        latest_status: { type: [string, "null"], enum: [passed, failed, timed_out, error, null] }
        runs:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              command_kind: { type: string, enum: [imports, typecheck, build, test] }
              status: { type: string, enum: [passed, failed, timed_out, error] }
              base_sha: { type: string }
              tree_sha: { type: string }
              exit_code: { type: [integer, "null"] }
              duration_ms: { type: integer }
              summary: { type: string }
              output_truncated: { type: boolean }
    PathItem:
      oneOf:
        - { type: string }
        - type: object
          required: [path]
          properties:
            path: { type: string }
            is_new_path: { type: boolean }
            start_line: { type: integer, minimum: 1, description: Inclusive; concrete files only; requires end_line. }
            end_line: { type: integer, minimum: 1, description: Inclusive; requires start_line and must be >= it. }
    LeasePathRange:
      type: object
      required: [path, start_line, end_line]
      properties:
        path: { type: string }
        start_line: { type: [integer, "null"], minimum: 1 }
        end_line: { type: [integer, "null"], minimum: 1 }
    AcquireLeaseRequest:
      type: object
      required: [repo, task_id]
      anyOf:
        - required: [paths]
        - required: [description]
      properties:
        repo: { type: string, pattern: "^[^/]+/[^/]+$" }
        task_id: { type: string }
        description: { type: string }
        paths: { type: array, items: { $ref: "#/components/schemas/PathItem" } }
        mode: { type: string, enum: [exclusive, shared-read] }
        ttl_seconds: { type: integer, minimum: 60, maximum: 86400 }
        agent_instance_id: { type: string }
    LeaseSummary:
      type: object
      required: [lease_id, status, paths]
      properties:
        lease_id: { type: string }
        status: { type: string }
        paths: { type: array, items: { type: string } }
        path_ranges: { type: array, items: { $ref: "#/components/schemas/LeasePathRange" } }
        queue_position: { type: integer }
        blocked_by_lease_ids: { type: array, items: { type: string } }
    AcquireLeaseResponse:
      allOf:
        - $ref: "#/components/schemas/LeaseSummary"
        - type: object
          required: [mode, path_ranges, excluded_paths, ttl, hotspot_leases]
          properties:
            mode: { type: string }
            path_ranges: { type: array, items: { $ref: "#/components/schemas/LeasePathRange" } }
            excluded_paths: { type: array, items: { type: string } }
            ttl: { type: integer }
            ttl_expires_at: { type: [integer, "null"] }
            hotspot_leases: { type: array, items: { $ref: "#/components/schemas/LeaseSummary" } }
    LeaseView:
      allOf:
        - $ref: "#/components/schemas/LeaseSummary"
        - type: object
          required: [task_id, mode, path_ranges, excluded_paths, ttl_seconds, is_hotspot]
          properties:
            task_id: { type: string }
            task_external_id: { type: [string, "null"] }
            mode: { type: string }
            path_ranges: { type: array, items: { $ref: "#/components/schemas/LeasePathRange" } }
            excluded_paths: { type: array, items: { type: string } }
            ttl_seconds: { type: integer }
            ttl_expires_at: { type: [integer, "null"] }
            is_hotspot: { type: boolean }
    HeartbeatResponse:
      type: object
      required: [lease_id, status]
      properties:
        lease_id: { type: string }
        status: { type: string }
        ttl_expires_at: { type: [integer, "null"] }
    TerritoryMapResponse:
      type: object
      required: [data, next_cursor, hotspots, queue_depth]
      properties:
        data: { type: array, items: { $ref: "#/components/schemas/LeaseView" } }
        next_cursor: { type: [string, "null"] }
        hotspots: { type: array, items: { type: object, additionalProperties: true } }
        queue_depth: { type: integer }
    ErrorEnvelope:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [type, message]
          properties:
            type: { type: string }
            message: { type: string }
