Project Resources

Attach typed pointers — a Git repo today, document links tomorrow — to a project so agents pick them up as scoped context.

A Project Resource is a typed pointer attached to a project — a Git repo URL today, a Notion page or document link in the future. When an agent runs against an issue inside that project, the daemon writes the resource list into the agent's working directory and into its meta-skill prompt automatically.

The effect: the agent already knows which repo to check out and which docs are the primary references for the project — no one needs to copy-paste context into the issue body.

Mental model

A project is no longer just a label — it is a small container of resources:

  • A project has 0..N resources.
  • Each resource has a resource_type (for example github_repo) and a resource_ref (a JSON payload typed by resource_type).
  • Adding a new resource type means adding a string plus a handler — no schema migration, no frontend rewrite.

The shape is intentional — it mirrors the pattern Statica already uses for agent providers: a type discriminator plus a typed payload. That keeps the schema stable, so adding "Notion page", "Google Doc", "uploaded file", or "external URL" later is a small additive change.

Today: github_repo

The first resource type ships ready to use:

{
  "resource_type": "github_repo",
  "resource_ref": {
    "url": "https://github.com/owner/repo",
    "default_branch_hint": "main"
  }
}

default_branch_hint is optional — when present, the daemon surfaces it in the meta-skill so the agent knows which branch to base its work on.

Attaching repos at project creation

In the Web or Desktop app, the New project dialog shows a Repos pill alongside Status / Priority / Lead. Selecting workspace-bound repos (or pasting an ad-hoc URL) attaches them as github_repo resources the moment the project is created.

From the CLI:

# Create and attach in one shot. The server attaches resources in the
# same transaction as the project create — invalid resources roll back
# the whole operation, so you never end up with half-attached projects.
statica project create \
  --title "Agent UX 2026" \
  --repo https://github.com/statica-ai/statica

# Manage resources later
statica project resource list <project-id>
statica project resource add  <project-id> --type github_repo --url <url>
statica project resource remove <project-id> <resource-id>

# Generic escape hatch for any resource_type the server understands —
# no CLI change needed when a new type ships:
statica project resource add <project-id> \
  --type notion_page \
  --ref '{"page_id":"…","title":"…"}'

--repo can be repeated; each value is attached as its own github_repo resource.

What the agent sees at runtime

When the daemon spawns an agent for an issue inside a project, two things happen:

1. .statica/project/resources.json — a structured pass-through of the API response, written into the agent's working directory:

{
  "project_id": "…",
  "project_title": "Agent UX 2026",
  "resources": [
    {
      "id": "…",
      "resource_type": "github_repo",
      "resource_ref": {
        "url": "https://github.com/statica-ai/statica",
        "default_branch_hint": "main"
      }
    }
  ]
}

Skills, helper scripts, or the agent itself can parse the file when they need the exact set of resources for the run.

2. A "Project Context" section in the meta-skill prompt — the agent's CLAUDE.md / AGENTS.md (depending on provider) now includes a human-readable summary:

## Project Context

This issue belongs to **Agent UX 2026**.

Project resources (also written to `.statica/project/resources.json`):

- **GitHub repo**: https://github.com/statica-ai/statica (default branch: `main`)

Resources are pointers — open them only when relevant to the task. For
`github_repo` resources, use `statica repo checkout <url>` to fetch the code.

The prose is deliberately minimal. The full payload lives on disk; the prompt only orients the agent so it knows the project exists and what is attached.

Failure mode

Resource fetch is best-effort. If the API call fails, the project section is omitted from the prompt and the file is not written, but the task still starts. Agents never block on missing project context.

Adding a new resource type

The whole point of the abstraction is that new types are cheap. The full path:

  1. Server validator (server/internal/handler/project_resource.go) — add a case in validateAndNormalizeResourceRef that parses and normalizes the new payload.
  2. Daemon meta-skill formatter (server/internal/daemon/execenv/runtime_config.go) — add a case in formatProjectResource so the agent prompt renders the new type as a readable bullet.
  3. TypeScript types (packages/core/types/project.ts) — extend ProjectResourceType and add the payload interface.
  4. UI renderer (packages/views/projects/components/project-resources-section.tsx) — add a case in ResourceRow for the new type.

No schema migration, no new sqlc query, no new endpoint, no CLI change — the CLI's generic --ref '<json>' flag accepts any payload the validator understands, so day-one support for a new type is just the four steps above. A per-type CLI shortcut can be added later if you want, but is not required.

The same project_resource table and the same three CRUD calls handle every type.

Workspace repos vs. project repos

The repo list shown to the agent (the ## Repositories block in CLAUDE.md / AGENTS.md) is chosen by the daemon claim handler with this precedence:

  1. If the project has at least one github_repo resource → only those repos are surfaced to the agent. Workspace-bound repos are deliberately hidden so the agent doesn't have to guess which one belongs to the issue.
  2. If the project has no github_repo resources (or the issue is not in a project) → fall back to the workspace's repo list, the same as before.

This keeps the agent's working set tight: when a project is explicit about its repos, that's the authoritative answer. The structured resource list at .statica/project/resources.json still carries the full set, so a skill that wants to inspect everything still can.

The daemon mirrors the same precedence on the checkout side: when a task arrives with project-scoped github_repo URLs, those URLs are merged into the per-workspace allowlist and synced into the local repo cache before the agent spawns. So a project repo URL that is not bound at the workspace level is still a valid argument to statica repo checkout — the daemon will not reject it as "not configured." The allowlist split is internal: workspace-bound URLs and task-scoped URLs are tracked separately, so a workspace-repos refresh doesn't accidentally revoke a project URL mid-run.

What's intentionally not in scope here

  • Cross-project sharing. Each resource lives on exactly one project today.
  • Per-skill resource scoping. All resources are visible to every skill on the agent's run; type-aware filtering is a follow-up.
  • Caching / sync. github_repo is just metadata — checkout still happens via statica repo checkout on demand. Cached document text for Notion / Google Docs will arrive alongside those types.

These omissions are deliberate — the goal of the first cut is to validate the abstraction with the smallest possible set of moving parts.