Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.crewship.ai/llms.txt

Use this file to discover all available pages before exploring further.

Single endpoint that forces an immediate run of the memory consolidation worker. Requires authentication, workspace scope, and OWNER or ADMIN role. See the Consolidate guide. The scheduled runner ticks every 6h regardless; this endpoint exists so operators can force an immediate pass after curating a crew or auditing new rules.

Trigger run

POST /api/v1/consolidate/run
Auth: OWNER or ADMIN only (403 otherwise). Request body (optional):
{
  "crew_id": "crw_backend",
  "since": "24h"
}
FieldTypeRequiredDefaultDescription
crew_idstringNoall crewsLimit run to a single crew. Must be live in the caller’s workspace.
sincestringNo24hLook-back window. Go duration (90m, 24h) plus shorthand d (days) and w (weeks). Zero/negative falls back to 24h.
Body may be empty; the defaults run consolidation across every non-deleted crew in the workspace for the last 24h. Response: 202 Accepted — run started:
{
  "triggered": true,
  "worker_id": "csd_a1b2c3d4e5f6"
}
Response: 202 Accepted — no summarizer configured, run skipped:
{
  "accepted": true,
  "note": "no summarizer configured, skipping"
}
In both cases the journal records a system.consolidation_triggered + system.consolidation_completed pair so the audit trail is complete. Errors:
StatusCondition
400Invalid JSON body.
401Not authenticated.
403Not OWNER/ADMIN.
404crew_id not in your workspace, or soft-deleted.
409A consolidation is already running for this workspace (per-workspace in-flight guard).
500DB error.
503Consolidator not wired in this build (dev/test).

Preview proposal diff

GET /api/v1/consolidate/proposed/{id}/diff
Auth: MEMBER or higher (matches Explain — no write authority needed to preview). Returns a 3-line-context unified diff between the current canonical learned-YYYY-MM-DD.md and the file an approve of {id} would land on disk. The post-merge half of the diff is byte-identical to what POST /api/v1/consolidate/proposed/{id}/approve would write (modulo the per-instant Approved at HH:MM:SS line, which races the wall clock between preview and approve by definition). That equality is the load-bearing UX promise — a reviewer can read the diff, click approve, and trust that the committed bytes match what they saw.
curl -H "Authorization: Bearer $TOKEN" \
  https://crewship.example.com/api/v1/consolidate/proposed/mp_8f3a2b1c/diff
Response: 200 OK:
{
  "proposal_id": "mp_8f3a2b1c",
  "workspace_id": "wks_a1b2c3",
  "crew_id": "crw_backend",
  "status": "pending",
  "canonical_path": "/var/lib/crewship/memory/crw_backend/topics/learned-2026-05-18.md",
  "canonical_exists": true,
  "proposal_path": "/var/lib/crewship/memory/crw_backend/topics/.proposed/proposal-mp_8f3a2b1c.md",
  "rules_count": 3,
  "diff": "--- canonical (current)\n+++ canonical (post-merge)\n@@ -12,3 +12,17 @@\n - Always pin migration versions by name, not number.\n - Reload SecretStore after credential rotation.\n - Keeper L1 with >=10-char intent auto-allows.\n+\n+## Approved 2026-05-18 (Approved at 14:22:07 UTC)\n+\n+- Retry sidecar /assign on 503 with exponential backoff.\n+- Treat cross-workspace 404 as deny; do not leak existence.\n+- Cap proposal markdown reads at 8 MB.\n",
  "stats": {
    "additions": 14,
    "deletions": 0,
    "rules_appended": 3
  }
}
FieldTypeDescription
proposal_idstringEcho of {id}.
workspace_idstringOwning workspace; always matches the caller’s scope (cross-workspace probes are 404).
crew_idstringCrew the proposal belongs to.
statusstringCarries through from the row — pending, approved, or rejected. Previewing an already-resolved proposal is permitted (operator double-click is fine).
canonical_pathstringAbsolute path the merge would land on, derived from today’s UTC date.
canonical_existsbooleanfalse on first-time merges for the day; the diff in that case is pure additions against an empty file.
proposal_pathstringBack-link to the .proposed/proposal-*.md source file.
rules_countintegerCount of rules in the proposal, matches stats.rules_appended.
diffstringUnified diff body with --- canonical (current) / +++ canonical (post-merge) chrome. Empty string iff the merge is a no-op.
stats.additionsintegerContent lines starting with + (header lines excluded).
stats.deletionsintegerContent lines starting with - (header lines excluded). Always 0 today — the merge is append-only. The counter exists so a future dedup-aware merge surfaces removals without a schema change.
stats.rules_appendedintegerMirrors rules_count.
Errors:
StatusCondition
400Missing {id} path param.
401Not authenticated, or no workspace context.
404Unknown {id}, or proposal belongs to a different workspace (same 404 — no existence leak).
410Proposal row exists but the .proposed/proposal-*.md file is missing on disk (deleted out-of-band; recoverable by re-running the consolidator).
413Proposal markdown or canonical file exceeds the 8 MB read cap (DoS guard).
500Read error other than not-found, or diff builder failure.

Approve proposal

POST /api/v1/consolidate/proposed/{id}/approve
Auth: OWNER or ADMIN only (403 otherwise); workspace context required. The proposal id + caller identity are the entire state machine — any request body is ignored. The handler does a read-only ExplainProposal lookup first; a cross-workspace probe surfaces as 404 (no existence leak) and an already-decided row short-circuits with 409 before any state changes. On success the proposal’s rendered body is appended to the canonical learned-YYYY-MM-DD.md for today (UTC), the row flips to status='approved' with decided_at + decided_by_user_id, the matching inbox row resolves with action='approved', and memory.consolidated lands on the journal. When BlobRoot is wired (see Memory observability), a memory_versions audit row is also recorded against the post-merge canonical file.
curl -X POST -H "Authorization: Bearer $TOKEN" \
  https://crewship.example.com/api/v1/consolidate/proposed/mp_8f3a2b1c/approve
Response: 200 OK:
{
  "proposal_id": "mp_8f3a2b1c",
  "canonical_path": "/var/lib/crewship/memory/crw_backend/topics/learned-2026-05-18.md",
  "rules_merged": 3,
  "workspace_id": "wks_a1b2c3",
  "crew_id": "crw_backend",
  "decided_by": "usr_7d4e9f",
  "version_sha": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
FieldTypeDescription
proposal_idstringEcho of {id}.
canonical_pathstringAbsolute path the rules were merged into; derived from today’s UTC date.
rules_mergedintegerNumber of rules pulled from the proposal body into the canonical file. Matches the proposal’s rules_count.
workspace_idstringOwning workspace; always matches the caller’s scope.
crew_idstringCrew the proposal belonged to.
decided_bystringUser id that approved — the caller’s identity, lifted from auth context.
version_shastringContent-addressed sha256 of the post-merge canonical file. Omitted when versioning is disabled (BlobRoot unset) or the audit-row write failed best-effort (the approve itself still succeeded; correlate via the warn log).
Errors:
StatusCondition
400Missing {id} path param.
401Not authenticated, or no workspace context.
403Authenticated but not OWNER/ADMIN.
404Unknown {id}, or proposal belongs to a different workspace (same 404 — no existence leak).
409Proposal already decided (approved or rejected); idempotent retry is rejected so the body is not re-merged.
500Canonical append failed, DB transition failed, or unexpected lookup error.

Reject proposal

POST /api/v1/consolidate/proposed/{id}/reject
Auth: OWNER or ADMIN only (403 otherwise); workspace context required. Same workspace boundary pattern as approve — the lookup runs first, a cross-workspace probe is 404, an already-decided row is 409, and only then does the row flip to status='rejected'. The inbox item resolves with action='rejected' and the reason is logged at notice level so audit reviews don’t have to JOIN through memory_proposals for the “why”. The .proposed/proposal-*.md file stays on disk for audit; a separate retention sweep removes it. No memory.consolidated journal entry is emitted on reject — the inbox audit trail and the original memory.consolidation_proposed from the seed run carry the lineage. Request body (optional):
{
  "reason": "duplicates an existing rule in learned-2026-05-10.md"
}
FieldTypeRequiredDefaultDescription
reasonstringNo""Free-form rejection note. Malformed JSON body falls through to no reason (permissive happy path — the proposal still rejects).
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"reason":"duplicates an existing rule"}' \
  https://crewship.example.com/api/v1/consolidate/proposed/mp_8f3a2b1c/reject
Response: 200 OK:
{
  "proposal_id": "mp_8f3a2b1c",
  "status": "rejected",
  "decided_by": "usr_7d4e9f",
  "reason": "duplicates an existing rule"
}
FieldTypeDescription
proposal_idstringEcho of {id}.
statusstringAlways "rejected" on a 200.
decided_bystringUser id that rejected — the caller’s identity.
reasonstringEcho of the request body’s reason; empty string when no body, an unparseable body, or an explicit empty field.
Errors:
StatusCondition
400Missing {id} path param.
401Not authenticated, or no workspace context.
403Authenticated but not OWNER/ADMIN.
404Unknown {id}, or proposal belongs to a different workspace (same 404 — no existence leak).
409Proposal already decided.
500DB transition failed, or unexpected lookup error.

Explain proposal

GET /api/v1/consolidate/proposed/{id}/explain
Auth: MEMBER or higher inside the workspace — read-only, no write authority needed to review what was proposed. Returns the full proposal row plus the evidence the summarizer LLM looked at, so the HITL review UI can show “why this rule, given which journal entries”. Cross-workspace probes return 404 (no existence leak). The endpoint is safe to call on already-decided proposals; status, decided_at, and decided_by_user_id carry the resolution. scores is the per-rule ScoreResult map populated by the proposal writer once scoring is wired — proposals from before that landed return a literal {} so clients can blindly Unmarshal without nil-checks.
curl -H "Authorization: Bearer $TOKEN" \
  https://crewship.example.com/api/v1/consolidate/proposed/mp_8f3a2b1c/explain
Response: 200 OK:
{
  "proposal_id": "mp_8f3a2b1c",
  "workspace_id": "wks_a1b2c3",
  "crew_id": "crw_backend",
  "status": "pending",
  "proposal_path": "/var/lib/crewship/memory/crw_backend/topics/.proposed/proposal-mp_8f3a2b1c.md",
  "rules_count": 3,
  "entries_scanned": 42,
  "created_at": "2026-05-18T08:00:11.214Z",
  "evidence": [
    {"type": "peer.escalation", "id": "jrn_001", "summary": "..."},
    {"type": "keeper.decision", "id": "jrn_017", "summary": "..."}
  ],
  "scores": {
    "Always pin migration versions by name, not number.": {
      "evidence_count": 4,
      "recency_score": 0.81,
      "severity_score": 0.62,
      "final": 0.74
    }
  }
}
FieldTypeDescription
proposal_idstringEcho of {id}.
workspace_idstringOwning workspace; always matches the caller’s scope.
crew_idstringCrew the proposal belongs to.
statusstringpending, approved, or rejected.
proposal_pathstringAbsolute path to the rendered .proposed/proposal-*.md file.
rules_countintegerNumber of rules in the proposal body.
entries_scannedintegerHow many candidate journal entries the summarizer pass considered before extracting the rules.
created_atstringRFC3339 timestamp from the memory_proposals row.
decided_atstringRFC3339 timestamp; omitted while status == "pending".
decided_by_user_idstringUser id of the approver/rejector; omitted while status == "pending".
evidencejsonRaw JSON array of source journal entries the LLM looked at — shape is whatever the summarizer wrote into evidence_json (no re-shaping at the API boundary).
scoresjsonPer-rule ScoreResult map keyed by rule pattern. Literal {} for proposals created before scoring was wired, or when running against a pre-v92 schema (the handler detects column presence once and falls back to '{}' literal).
Errors:
StatusCondition
400Missing {id} path param.
401No workspace context.
404Unknown {id}, or proposal belongs to a different workspace (same 404 — no existence leak).
500DB query failure.

Side effects

A successful run:
  1. Loads recent journal entries per crew (filtered to candidate types: peer.escalation, summary.generated, keeper.decision, mission.status_change, eval.regression_detected).
  2. Calls the summarizer LLM (Ollama in production).
  3. Appends extracted rules to {memoryRoot}/{crewSlug}/topics/learned-YYYY-MM-DD.md.
  4. Emits memory.consolidated per crew with rules_count and output path in the payload.
  5. Emits system.consolidation_completed once with aggregate crews_run and rules_appended.

Scheduling

Background scheduler: every 6h. Compactor: daily at 03:00 UTC. Both wired at server startup via consolidate.StartBackground.