doc/SPEC-implementation.md
Status: Implementation contract for first release (V1)
Date: 2026-04-28
Audience: Product, engineering, and agent-integration authors
Source inputs: GOAL.md, PRODUCT.md, SPEC.md, DATABASE.md, current monorepo code
SPEC.md remains the long-horizon product spec.
This document is the concrete, build-ready V1 contract.
When there is a conflict, SPEC-implementation.md controls V1 behavior.
Paperclip V1 must provide a full control-plane loop for autonomous agents:
Success means one operator can run a small AI-native company end-to-end with clear visibility and control.
These decisions close open questions from SPEC.md for V1.
| Topic | V1 Decision |
|---|---|
| Tenancy | Single-tenant deployment, multi-company data model |
| Company model | Company is first-order; all business entities are company-scoped |
| Board | Single human board operator per deployment |
| Org graph | Strict tree (reports_to nullable root); no multi-manager reporting |
| Visibility | Full visibility to board and all agents in same company |
| Communication | Tasks + comments only (no separate chat system) |
| Task ownership | Single assignee; atomic checkout required for in_progress transition |
| Recovery | Liveness/watchdog recovery preserves explicit ownership: retry lost execution continuity where safe, otherwise create visible recovery issues or require human escalation (see doc/execution-semantics.md) |
| Agent adapters | Built-in process, http, local CLI/session adapters, and OpenClaw gateway support; external adapters can also be loaded through the adapter plugin flow |
| Plugin framework | Local/self-hosted early plugin runtime is in scope; cloud marketplace and packaged public distribution remain out of scope |
| Auth | Mode-dependent human auth (local_trusted implicit board in current code; authenticated mode uses sessions), API keys for agents |
| Budget period | Monthly UTC calendar window |
| Budget enforcement | Soft alerts + hard limit auto-pause |
| Deployment modes | Canonical model is local_trusted + authenticated with private/public exposure policy (see doc/DEPLOYMENT-MODES.md) |
As of 2026-02-17, the repo already includes:
agents, projects, goals, issues, activityDATABASE_URL is unsetV1 implementation extends this baseline into a company-centric, governance-aware control plane.
server/: REST API, auth, orchestration servicesui/: Board operator interfacepackages/db/: Drizzle schema, migrations, DB clients (Postgres)packages/shared/: Shared API types, validators, constants~/.paperclip/instances/default/db~/.paperclip/instances/default/data/storage (local_disk)s3)A lightweight scheduler/worker in the server process handles:
Separate queue infrastructure is not required for V1.
All core tables include id, created_at, updated_at unless noted.
Human auth tables (users, sessions, and provider-specific auth artifacts) are managed by the selected auth library. This spec treats them as required dependencies and references users.id where user attribution is needed.
companiesid uuid pkname text not nulldescription text nullstatus enum: active | paused | archivedpause_reason text nullpaused_at timestamptz nullissue_prefix text not nullissue_counter int not nullbudget_monthly_cents int not null default 0spent_monthly_cents int not null default 0attachment_max_bytes int not nullrequire_board_approval_for_new_agents boolean not null default falsebrand_colorInvariant: every business record belongs to exactly one company.
agentsid uuid pkcompany_id uuid fk companies.id not nullname text not nullrole text not nulltitle text nullicon text nullstatus enum: active | paused | idle | running | error | pending_approval | terminatedreports_to uuid fk agents.id nullcapabilities text nulladapter_type text; built-ins include process, http, claude_local, codex_local, gemini_local, opencode_local, pi_local, cursor, and openclaw_gatewayadapter_config jsonb not nullruntime_config jsonb not null default {}default_environment_id uuid fk environments.id nullcontext_mode enum: thin | fat default thinbudget_monthly_cents int not null default 0spent_monthly_cents int not null default 0pause_reason, paused_atpermissions jsonb not null default {}last_heartbeat_at timestamptz nullmetadata jsonb nullInvariants:
terminated agents cannot be resumedagent_api_keysid uuid pkagent_id uuid fk agents.id not nullcompany_id uuid fk companies.id not nullname text not nullkey_hash text not nulllast_used_at timestamptz nullrevoked_at timestamptz nullInvariant: plaintext key shown once at creation; only hash stored.
goalsid uuid pkcompany_id uuid fk not nulltitle text not nulldescription text nulllevel enum: company | team | agent | taskparent_id uuid fk goals.id nullowner_agent_id uuid fk agents.id nullstatus enum: planned | active | achieved | cancelledInvariant: at least one root company level goal per company.
projectsid uuid pkcompany_id uuid fk not nullgoal_id uuid fk goals.id nullname text not nulldescription text nullstatus enum: backlog | planned | in_progress | completed | cancelledlead_agent_id uuid fk agents.id nulltarget_date date nullenv jsonb null (same secret-aware env binding format used by agent config)Invariant:
issues (core task entity)id uuid pkcompany_id uuid fk not nullproject_id uuid fk projects.id nullproject_workspace_id uuid fk project_workspaces.id nullgoal_id uuid fk goals.id nullparent_id uuid fk issues.id nulltitle text not nulldescription text nullstatus enum: backlog | todo | in_progress | in_review | done | blocked | cancelledpriority enum: critical | high | medium | lowassignee_agent_id uuid fk agents.id nullassignee_user_id text nullcheckout_run_id, execution_run_id, execution_agent_name_key, execution_locked_atcreated_by_agent_id uuid fk agents.id nullcreated_by_user_id uuid fk users.id nullissue_number, identifierorigin_kind, origin_id, origin_run_id, origin_fingerprintrequest_depth int not null default 0billing_code text nullassignee_adapter_overrides jsonb nullexecution_policy jsonb nullexecution_state jsonb nullexecution_workspace_id, execution_workspace_preference, execution_workspace_settingsstarted_at timestamptz nullcompleted_at timestamptz nullcancelled_at timestamptz nullhidden_at timestamptz nullInvariants:
goal_id, parent_id, or project-goal linkagein_progress requires assigneedone | cancelledissue_commentsid uuid pkcompany_id uuid fk not nullissue_id uuid fk issues.id not nullauthor_agent_id uuid fk agents.id nullauthor_user_id uuid fk users.id nullbody text not nullheartbeat_runsid uuid pkcompany_id uuid fk not nullagent_id uuid fk not nullinvocation_source enum: scheduler | manual | callbackstatus enum: queued | running | succeeded | failed | cancelled | timed_outstarted_at timestamptz nullfinished_at timestamptz nullerror text nullexternal_run_id text nullcontext_snapshot jsonb nullcost_eventsid uuid pkcompany_id uuid fk not nullagent_id uuid fk agents.id not nullissue_id uuid fk issues.id nullproject_id uuid fk projects.id nullgoal_id uuid fk goals.id nullbilling_code text nullprovider text not nullmodel text not nullinput_tokens int not null default 0output_tokens int not null default 0cost_cents int not nulloccurred_at timestamptz not nullInvariant: each event must attach to agent and company; rollups are aggregation, never manually edited.
approvalsid uuid pkcompany_id uuid fk not nulltype enum: hire_agent | approve_ceo_strategy | budget_override_required | request_board_approvalrequested_by_agent_id uuid fk agents.id nullrequested_by_user_id uuid fk users.id nullstatus enum: pending | revision_requested | approved | rejected | cancelledpayload jsonb not nulldecision_note text nulldecided_by_user_id uuid fk users.id nulldecided_at timestamptz nullactivity_logid uuid pkcompany_id uuid fk not nullactor_type enum: agent | user | systemactor_id uuid/text not nullaction text not nullentity_type text not nullentity_id uuid/text not nulldetails jsonb nullcreated_at timestamptz not null default now()company_secrets + company_secret_versionsagents.adapter_config.env.company_secrets tracks identity/provider metadata per company.company_secret_versions stores encrypted/reference material per version.local_encrypted.Operational policy:
agents(company_id, status)agents(company_id, reports_to)issues(company_id, status)issues(company_id, assignee_agent_id, status)issues(company_id, parent_id)issues(company_id, project_id)cost_events(company_id, occurred_at)cost_events(company_id, agent_id, occurred_at)heartbeat_runs(company_id, agent_id, started_at desc)approvals(company_id, status, type)activity_log(company_id, created_at desc)assets(company_id, created_at desc)assets(company_id, object_key) uniqueissue_attachments(company_id, issue_id)company_secrets(company_id, name) uniquecompany_secret_versions(secret_id, version) uniqueassets + issue_attachmentsassets stores provider-backed object metadata (not inline bytes):
id uuid pkcompany_id uuid fk not nullprovider enum/text (local_disk | s3)object_key text not nullcontent_type text not nullbyte_size int not nullsha256 text not nulloriginal_filename text nullcreated_by_agent_id uuid fk nullcreated_by_user_id uuid/text fk nullissue_attachments links assets to issues/comments:
id uuid pkcompany_id uuid fk not nullissue_id uuid fk not nullasset_id uuid fk not nullissue_comment_id uuid fk nulldocuments + document_revisions + issue_documentsdocuments stores editable text-first documents:
id uuid pkcompany_id uuid fk not nulltitle text nullformat text not null (markdown)latest_body text not nulllatest_revision_id uuid nulllatest_revision_number int not nullcreated_by_agent_id uuid fk nullcreated_by_user_id uuid/text fk nullupdated_by_agent_id uuid fk nullupdated_by_user_id uuid/text fk nulldocument_revisions stores append-only history:
id uuid pkcompany_id uuid fk not nulldocument_id uuid fk not nullrevision_number int not nullbody text not nullchange_summary text nullissue_documents links documents to issues with a stable workflow key:
id uuid pkcompany_id uuid fk not nullissue_id uuid fk not nulldocument_id uuid fk not nullkey text not null (plan, design, notes, etc.)The current implementation includes additional V1-control-plane tables beyond the original February snapshot:
issue_relations for blockers, labels/issue_labels, issue_thread_interactions, issue_approvals, issue_execution_decisions, issue_work_products, issue_inbox_archives, issue_read_states, and issue reference mention indexes.execution_workspaces, project_workspaces, workspace_runtime_services, workspace_operations, environments, environment_leases, agent_task_sessions, agent_runtime_state, agent_wakeup_requests, heartbeat events, and watchdog decision tables.plugins, plugin config/state/entities/jobs/logs/webhooks, plugin database namespaces/migrations, plugin company settings, and routines.Allowed transitions:
idle -> runningrunning -> idlerunning -> errorerror -> idleidle -> pausedrunning -> paused (requires cancel flow)paused -> idle* -> terminated (board only, irreversible)Allowed transitions:
backlog -> todo | cancelledtodo -> in_progress | blocked | cancelledin_progress -> in_review | blocked | done | cancelledin_review -> in_progress | done | cancelledblocked -> todo | in_progress | cancelleddone, cancelledSide effects:
in_progress sets started_at if nulldone sets completed_atcancelled sets cancelled_atV1 non-terminal liveness rule:
todo, in_progress, in_review, and blocked issues must have a live execution path, an explicit waiting path, or an explicit recovery pathin_review is healthy only when a typed execution participant, pending issue-thread interaction or approval, user owner, active run, queued wake, or explicit recovery issue owns the next actionDetailed ownership, execution, blocker, active-run watchdog, crash-recovery, and non-terminal liveness semantics are documented in doc/execution-semantics.md.
pending -> approved | rejected | cancelledactivity_log| Action | Board | Agent |
|---|---|---|
| Create company | yes | no |
| Hire/create agent | yes (direct) | request via approval |
| Pause/resume agent | yes | no |
| Create/update task | yes | yes |
| Force reassign task | yes | limited |
| Approve strategy/hire requests | yes | no |
| Report cost | yes | yes |
| Set company budget | yes | no |
| Set subordinate budget | yes | yes (manager subtree only) |
All endpoints are under /api and return JSON.
GET /companiesPOST /companiesGET /companies/:companyIdPATCH /companies/:companyIdPATCH /companies/:companyId/brandingPOST /companies/:companyId/archiveGET /companies/:companyId/goalsPOST /companies/:companyId/goalsGET /goals/:goalIdPATCH /goals/:goalIdDELETE /goals/:goalId (soft delete optional, hard delete board-only)GET /companies/:companyId/agentsPOST /companies/:companyId/agentsGET /agents/:agentIdPATCH /agents/:agentIdPOST /agents/:agentId/pausePOST /agents/:agentId/resumePOST /agents/:agentId/terminatePOST /agents/:agentId/keys (create API key)POST /agents/:agentId/heartbeat/invokeGET /companies/:companyId/issuesPOST /companies/:companyId/issuesGET /issues/:issueIdPATCH /issues/:issueIdGET /issues/:issueId/documentsGET /issues/:issueId/documents/:keyPUT /issues/:issueId/documents/:keyGET /issues/:issueId/documents/:key/revisionsDELETE /issues/:issueId/documents/:keyPOST /issues/:issueId/checkoutPOST /issues/:issueId/releasePOST /issues/:issueId/admin/force-release (board-only lock recovery)POST /issues/:issueId/commentsGET /issues/:issueId/commentsPOST /companies/:companyId/issues/:issueId/attachments (multipart upload)GET /issues/:issueId/attachmentsGET /attachments/:attachmentId/contentDELETE /attachments/:attachmentIdPOST /issues/:issueId/checkout request:
{
"agentId": "uuid",
"expectedStatuses": ["todo", "backlog", "blocked", "in_review"]
}
Server behavior:
WHERE id = ? AND status IN (?) AND (assignee_agent_id IS NULL OR assignee_agent_id = :agentId)409 with current owner/statusassignee_agent_id, status = in_progress, and started_atPOST /issues/:issueId/admin/force-release is an operator recovery endpoint for stale harness locks. It requires board access to the issue company, clears checkout and execution run lock fields, and may clear the agent assignee when clearAssignee=true is passed. The route must write an issue.admin_force_release activity log entry containing the previous checkout and execution run IDs.
GET /companies/:companyId/projectsPOST /companies/:companyId/projectsGET /projects/:projectIdPATCH /projects/:projectIdGET /companies/:companyId/approvals?status=pendingPOST /companies/:companyId/approvalsPOST /approvals/:approvalId/approvePOST /approvals/:approvalId/rejectPOST /companies/:companyId/cost-eventsGET /companies/:companyId/costs/summaryGET /companies/:companyId/costs/by-agentGET /companies/:companyId/costs/by-projectPATCH /companies/:companyId/budgetsPATCH /agents/:agentId/budgetsGET /companies/:companyId/activityGET /companies/:companyId/dashboardDashboard payload must include:
400 validation error401 unauthenticated403 unauthorized404 not found409 state conflict (checkout conflict, invalid transition)422 semantic rule violation500 server errorThe current app also exposes V1-supporting surfaces for:
suggest_tasks, ask_user_questions, request_confirmation)interface AgentAdapter {
invoke(agent: Agent, context: InvocationContext): Promise<InvokeResult>;
status(run: HeartbeatRun): Promise<RunStatus>;
cancel(run: HeartbeatRun): Promise<void>;
}
Config shape:
{
"command": "string",
"args": ["string"],
"cwd": "string",
"env": {"KEY": "VALUE"},
"timeoutSec": 900,
"graceSec": 15
}
Behavior:
Config shape:
{
"url": "https://...",
"method": "POST",
"headers": {"Authorization": "Bearer ..."},
"timeoutMs": 15000,
"payloadTemplate": {"agentId": "{{agent.id}}", "runId": "{{run.id}}"}
}
Behavior:
thin: send IDs and pointers only; agent fetches context via APIfat: include current assignments, goal summary, budget snapshot, and recent commentsPer-agent schedule fields in adapter_config:
enabled booleanintervalSec integer (minimum 30)maxConcurrentRuns integer; new agents default to 5Scheduler must skip invocation when:
approval(type=hire_agent, status=pending, payload=agent draft).activity_log.Board can bypass request flow and create agents directly via UI; direct create is still logged as a governance action.
approval(type=approve_ceo_strategy).Before first strategy approval, CEO may only draft tasks, not transition them to active execution states.
Board can at any time:
pausedBoard may override by raising budget or explicitly resuming agent.
POST /companies/:companyId/cost-events body:
{
"agentId": "uuid",
"issueId": "uuid",
"provider": "openai",
"model": "gpt-5",
"inputTokens": 1234,
"outputTokens": 567,
"costCents": 89,
"occurredAt": "2026-02-17T20:25:00Z",
"billingCode": "optional"
}
Validation:
costCents >= 0Read-time aggregate queries are acceptable for V1. Materialized rollups can be added later if query latency exceeds targets.
V1 UI routes:
/ dashboard/companies company list/create/companies/:id/org org chart and agent status/companies/:id/tasks task list/kanban/companies/:id/agents/:agentId agent detail/companies/:id/costs cost and budget dashboard/companies/:id/approvals pending/history approvals/companies/:id/activity audit/event streamRequired UX behaviors:
DATABASE_URL optional~/.paperclip/instances/default/dbpnpm db:migrate applies pending migrations manuallyactivity_logadapter_config, auth headers, env vars)A release candidate is blocked unless these pass:
Current implementation note: the milestones below describe the original V1 sequencing. Several systems originally framed as future work have since shipped or advanced materially, including issue documents/interactions, blockers, routines, execution workspaces, import/export portability, authenticated deployment modes, multi-user basics, and the local/self-hosted plugin runtime.
companies and company scoping to existing entitiesprocess adapter with cancel semanticshttp adapter with timeout/error handlingV1 is complete only when all criteria are true:
409 on concurrent claims.DATABASE_URL.V1 supports company import/export using a portable package contract:
COMPANY.md.paperclip.yaml sidecar for Paperclip-specific fidelitydocs/companies/companies-spec.mdagents/<slug>/AGENTS.mdteams/<slug>/TEAM.mdprojects/<slug>/PROJECT.mdprojects/<slug>/tasks/<slug>/TASK.mdtasks/<slug>/TASK.mdskills/<slug>/SKILL.mdExport/import behavior in V1:
.paperclip.yamlTASK.md entries use recurring: true in the base package and Paperclip routine fidelity in .paperclip.yamlcwd, local instruction file paths, inline prompt duplication) while preserving portable project repo/workspace metadata such as repoUrl, refs, and workspace-policy references keyed in .paperclip.yamlrename, skip, replace