Skip to main content
Glama
README.md
# open-projects

High-level project management CLI, MCP server, and SDK for AI coding agents. A project can be any repository, app, docs folder, scaffold, experiment, or remote-intended project in any folder. Projects tracks roots, recipes, agents, tmux profiles, immutable project events, and prompt-driven AI agent runs.

## Project Resources

- [Changelog](CHANGELOG.md)
- [Security policy](SECURITY.md)
- [Contributing guide](CONTRIBUTING.md)
- [Code of conduct](CODE_OF_CONDUCT.md)

## Install

```bash
bun install -g @hasna/projects
```

## CLI

The app and binary are named `projects`, and the public command surface is project-first.

```bash
# Prompt mode through AI SDK + OpenRouter
projects --dry-run --json "Plan a new open source project named Log Tools in /tmp/log-tools with tmux"
projects --yes "Create a docs project in /home/me/docs/new-docs and write a marker"
projects --model openai/gpt-4o-mini --max-steps 8 "Import this folder as a project"

# Roots
projects roots add --name "Open Source" --path /home/me/opensource --kind open-source --path-template "{slug}"
projects roots list --json
projects roots match --path /home/me/opensource/my-app --json
projects roots update open-source --github-org hasna --visibility public

# Recipes
projects recipes add --name "TypeScript Library" --kind open-source --tags typescript,library
projects recipes list
projects recipes built-ins
projects recipes seed-defaults --json

# Agents
projects agents add --name "Codex" --kind ai --provider openrouter --model openai/gpt-4o-mini
projects agents list

# Tmux profiles
projects tmux-profiles add --name "Dev" --slug dev --session-template "{slug}-dev" \
  --windows-json '[{"name":"editor"},{"name":"server","command":"bun run dev"}]'
projects tmux-profiles apply dev my-project --dry-run --json

# Projects
projects create --name "My App" --path /path/to/my-app --stage active --priority high \
  --owner hasna --launch-profile dev --start-agent claude --start-command "claude --resume" \
  --start-session-policy error-if-running \
  --start-windows-json '[{"name":"server","command":"bun run dev"}]' \
  --todos-project-id todo_123 --brief-id brief_123 --mkdir --git-init --marker --json
projects create --name "Planned App" --path /tmp/planned --mkdir --dry-run --json
projects create --name "Store Work" --kind project --mkdir --marker --json  # defaults to $HASNA_PROJECTS_HOME/workspaces/<id>
projects start                              # from inside a registered repo
projects start open-notes                  # by slug/id/name/path
projects start --json                      # operational structured output
projects start --render-spec               # validated JSON Render spec
projects start my-app --agent codewith
projects start /path/to/existing --agent claude
projects start my-app --windows-json '[{"name":"editor","command":"code ."},{"name":"server","command":"bun run dev"}]'
projects start /path/to/new-folder --tags family,security --metadata-json '{"domain":"home-security"}' --dry-run --json
projects start my-app --profile dev --agent claude --new
projects start my-app --error-if-running --agent none
projects start --rename-report --agent codewith
projects sessions my-app --unrenamed --json
projects start --bulk my-app docs-site service-api --agent opencode --dry-run --json
projects start --bulk-file ./project-targets.json --agent claude --dry-run --json
projects start --label kind:work-project --dry-run --json
projects status my-app --profile dev --json
projects store inspect my-app --json
projects canvases list my-app --ensure-default --render-spec
projects canvases create my-app --name "Research Board" --nodes-json '[{"id":"note","position":{"x":0,"y":0},"data":{"title":"Note"}}]'
projects canvases upsert my-app --slug planning-board --nodes-json '[{"id":"summary","type":"projectPanel","position":{"x":0,"y":0},"data":{"title":"Summary"}}]'
projects canvases compose my-app --spec-json '{"slug":"directory","blocks":[{"id":"summary","title":"Summary"},{"id":"table","title":"Table","columns":["name","role"],"rows":[{"name":"Ada","role":"Lead"}]}],"links":[{"source":"summary","target":"table"}]}'
projects dashboard snapshot my-app --write --json
projects dashboard render my-app --json
projects dashboard validate my-app --json
PROJECTS_DASHBOARD_TOKEN="<token>" projects dashboard serve my-app --host 0.0.0.0 --port 3344
projects reports serve --port 3345
PROJECTS_REPORTS_TOKEN="<token>" projects reports serve --host 0.0.0.0 --port 3345
projects loops link my-app daily-check --name "Daily Check" --role maintenance
projects loops list my-app --json
projects import /path/to/existing --json
projects import-github hasna/example --root open-source --clone --dry-run --json
projects scan-roots --root open-source --repo-prefix project- --clone --json
projects sync-roots --root open-source --repo-prefix project- --tags open-source,project --json
projects import /path/to/root --bulk --dry-run --json
projects list --query app --tags web,ts
projects list --label org:hasnaxyz --json
projects list --limit 50 --verbose
projects list --query app --tags web,ts --json
projects show my-app --json
projects show my-app --verbose
projects events list my-app --limit 10 --verbose
projects get my-app --json
projects update my-app --description "New description" --tags web,ts --priority critical --launch-profile dev
projects tag my-app security cameras
projects untag my-app cameras
projects labels add my-app org:hasnaxyz kind:work-project client:foo
projects labels list my-app
projects labels remove my-app client:foo
projects oss matrix --root /home/me/opensource --prefix open- --json
projects oss matrix --root /home/me/opensource --limit 50 --no-prs --no-tasks
projects store inspect my-app --json
projects store ensure my-app --json
projects store ensure wks_exact_stable_id --dry-run --json  # required target form in API mode
projects store migrate my-app --json          # dry-run plan
projects store migrate my-app --apply --json  # explicit move/update
projects link my-app --github-url https://github.com/hasna/my-app --todos-project-id todo_123 --todos-task-list-id list_123
projects unlink my-app --todos --brief
projects locations add my-app /path/to/another-folder --label docs
projects locations list my-app
projects archive my-app
projects unarchive my-app
projects delete my-app

# Budgets
projects budgets set --project my-app --max-usd 5 --max-total-tokens 100000 --json
projects budgets remaining --project my-app --json
projects --budget-project my-app --run-budget-tokens 2000 --json "Plan the next release"

# Project checks
projects doctor my-app --fix --dry-run --json

# Agent evals
projects agent-eval --json
projects agent-eval --mock --json
projects agent-eval --case create-explicit-path,tmux-apply-existing --fail-on-error

# Agent assist — help coding agents orient, decide, and continue
projects context my-app --for-agent        # one-shot priming bundle
projects next my-app --for-agent           # suggested next actions
projects why my-app --for-agent            # resolution trace + fix tips
projects handoff my-app --for-agent        # cross-agent/machine handoff bundle
projects runs list my-app --for-agent      # prompt-agent run ledger
projects runs show <run-id> my-app         # full run detail + tool-call trace
# All six also emit JSON with -j/--json and are available as MCP tools:
#   projects_context, projects_next, projects_why, projects_handoff,
#   projects_runs_list, projects_runs_show

# Conversations channel linkage — every project has one channel
projects channel my-app                    # print the project's channel name
projects channel my-app --json             # full resolution (class, linked, source)
projects channel my-app --ensure           # create the channel if it does not exist
# The channel name lives on the project record as
# integrations.conversations_channel; when unset it IS the project slug,
# normalized. This CLI adds no prefix of its own and strips none — it does not
# own the fleet naming convention and will not carry a copy of it. (That is a
# statement about authority, not conformance: plenty of registry slugs do not
# match the convention. Repairing those is a project-rename decision, not a
# derivation one.) A project whose channel is not named after its slug says so
# by setting integrations.conversations_channel, which always wins.
#
# Ensure creates the channel but never writes the link back: pinning a derived
# name would outrank derivation forever and survive a revert. The link is set at
# project creation, or deliberately by an operator.
#
# The channel class comes from the project record too:
# integrations.conversations_channel_class if set, else the project kind, else
# unset — in which case no --class is sent and conversations picks the default.
# Most projects (kind `generic`) fall in that last bucket.
# `projects create` and `projects start` ensure the channel automatically
# (disable with PROJECTS_CHANNEL_ENSURE=0); MCP tool: projects_channel

# Shell completion, including workon
eval "$(projects completion)"
eval "$(projects completion --shell zsh)"
```

## Goal-continue Cursor hook (ralph-style)

A `stop` hook lives in `.cursor/hooks.json` + `.cursor/hooks/goal-continue.sh`.
When an agent stops, it checks for an active goal and, if incomplete, blocks the
stop with a continuation prompt that includes `projects next` suggestions —
modeled on the codewith `/goal` slash command but driven as a Cursor hook.

Goal sources (first wins): `./.hasna/goal.md`, `$HASNA_GOAL_FILE`,
`~/.hasna/goal.md`. Mark a goal complete by adding a `<!-- done -->` line.

Env knobs: `HASNA_GOAL_SKIP=1` (disable), `HASNA_GOAL_CONTINUE=0` (status only,
no continuation), `HASNA_GOAL_MAX_SUGGESTIONS=N`. The hook never fails closed.

Projects stores high-level management fields directly on the project record:
`stage`, `priority`, `owner`, `launch_profile`, `start_agent`,
`start_command`, `start_session_policy`, `start_windows`, todos links, and
brief links. `projects start` and `projects status` use those launch defaults
unless the command passes an explicit override. By default, `projects start`
detects the current repo/project, creates or reuses the project tmux session,
and ensures base windows named `01` and `02`. Window `01` is the managed
coding-agent/work window when a start command is launched; `02` is the
secondary workspace. Existing unrelated tmux windows are left alone. Pass
`--windows-json` or the MCP/API `windows` field to request the exact tmux
window names for a single start/status operation.

Machine-readable outputs for `projects list`, `projects show`/`projects get`,
`projects status`, `projects start`, `projects sessions`, `projects roots list`,
and `projects recipes list` can emit validated JSON Render specs with
`--render-spec`. Existing `--json` payloads preserve their operational fields and
render specs are available from `--render-spec` and matching `projects_render_*`
MCP tools. Claude starts are annotated with `--name` when safe.
Codewith, Cursor, and OpenCode rename support is reported as manual or
unsupported unless a stable programmatic rename path is available; Open Projects
does not force text into unknown panes. Use `projects start --rename-report` or
`projects sessions <project> --unrenamed` to inspect rename status. Detailed
execution still belongs in `todos`; long-form specs and decisions still belong
in `brief`.

## Workspace Store

Projects has one canonical physical workspace store under `HASNA_PROJECTS_HOME`,
defaulting to `~/.hasna/projects`:

- canonical workspace path: `$HASNA_PROJECTS_HOME/workspaces/<workspace_id>/`
- runtime data path: `$HASNA_PROJECTS_HOME/data/<workspace_id>/`
- common runtime children: `project.db`, `logs/`, `artifacts/`, and `context/`

Rootless new projects default to the canonical workspace path. Explicit
`--path`, registered roots, imports, and GitHub checkout roots keep their
requested paths for compatibility until a user runs an explicit store migration.
Slugs, names, and org labels are mutable metadata and never define the canonical
folder name.

`projects store inspect` reports the canonical workspace/data paths and whether
the current primary path is canonical. `projects store ensure` creates missing
workspace/data directories, initializes the machine-local `project.db`, and
only sets the canonical path as primary when the project had no primary path.
In API mode, ensure requires the complete stable `wks_...` id, reads the full
project through the producer-bounded guarded endpoint, and uses the guarded
conditional update/receipt path for a missing primary path. Slugs and partial
ids are refused before transport; the station-local store is never created from
an unbounded or mismatched registry response. `projects store migrate` remains
local-only and is dry-run by default; it
requires `--apply` or `--yes` to move an existing primary folder into
`workspaces/<id>`, writes a migration plan under `data/<id>`, preserves git
history by moving the directory, records the old path as a non-primary location,
updates `workspaces.primary_path` through the normal location API, rewrites the
marker, and verifies the canonical primary path exists.

Labels are project metadata/query filters stored in the existing normalized tag
list. Use labels such as `org:hasnaxyz`, `kind:work-project`, and `client:foo`;
they do not create canonical folders and are safe to add, remove, or rename.

Per-project app data lives in the canonical runtime data path at
`$HASNA_PROJECTS_HOME/data/<workspace_id>/project.db`. That project database
stores project-specific canvases, custom JSON data models/records, and OpenLoops
links. `projects canvases * --render-spec` emits a JSON Render contract for a
TypeScript React surface using Tailwind, shadcn components, and React Flow as an
infinite canvas; a project may have multiple canvases. Use
`projects canvases upsert <project> --slug <slug>` when an agent or script needs
idempotent create-or-update behavior for raw React Flow `nodes`/`edges`.
Use `projects canvases compose <project>` for higher-level generic block specs:
blocks become `ProjectCanvasCard` nodes, links become React Flow edges, and
table-like blocks can carry `columns`/`rows` without creating a one-off
domain-specific command.

Generic block specs are intentionally domain-neutral. The same shape can model a
reporting hierarchy, a project map, a directory table, a roadmap, or a handoff
board:

```json
{
  "slug": "directory",
  "name": "Directory",
  "layout": { "direction": "grid", "columns": 2 },
  "blocks": [
    {
      "id": "summary",
      "title": "Summary",
      "kind": "summary",
      "metrics": [{ "label": "People", "value": 2, "tone": "info" }]
    },
    {
      "id": "table",
      "title": "Directory Table",
      "kind": "table",
      "columns": ["name", "role"],
      "rows": [{ "name": "Ada", "role": "Lead" }]
    }
  ],
  "links": [{ "source": "summary", "target": "table", "label": "details" }]
}
```

Cloud-backed runtime support is explicit in storage status. The local SQLite
registry and each project's local `project.db` remain the active runtime stores
by default. A configured `HASNA_PROJECTS_DATABASE_URL` enables explicit global
registry `projects storage push`, `pull`, and `sync` commands against Postgres;
it does not move per-project canvases, data records, loop links, or asset files
out of `$HASNA_PROJECTS_HOME/data/<workspace_id>/`. See
`docs/cloud-storage-readiness-contract.md` for the migration approval gate.

`projects dashboard *` is the Projects-owned viewer surface for agent-managed
project folders. It standardizes `.hasna/project/` inside the project path,
collects provider panels from `todos`, `files`, `mailery`, `conversations`,
`knowledge`, `mementos`, and `reports`, adds a read-only actions panel, validates
the result as `hasna.project_snapshot.v1`, and renders it as a React Flow Canvas.
Snapshot, render, and validate are read-only unless `--write` is passed.
`projects dashboard serve <project>` serves `/dashboard` plus JSON APIs with an
HTTP-only same-origin cookie; binding to a non-loopback host requires
`--token`, `PROJECTS_DASHBOARD_TOKEN`, or an explicit `--trust-network` choice.
Do not put dashboard access tokens in URLs, render specs, reports, or task
evidence. Dashboard JSON never carries raw private document bodies or arbitrary
shell commands.

`projects reports serve` serves registered project report files from each
project `reports/` directory. It binds to `127.0.0.1` by default. Binding
reports to a non-loopback host requires `--token`, `PROJECTS_REPORTS_TOKEN`, or
an explicit `--trust-network` choice; token mode uses an HTTP-only same-origin
cookie or `Authorization: Bearer` header and does not accept tokens in URLs.

OpenLoops integration uses the `@hasna/loops` SDK as an optional peer. Runtime
commands that need live loop state load `@hasna/loops/sdk` dynamically, so Open
Projects can still manage projects when OpenLoops is not installed.

## OSS Routing Matrix

`projects oss matrix` emits a compact routing matrix for direct child
repositories under an OSS workspace root. It is designed for orchestration
prompts and dispatch loops that need a capped, machine-readable snapshot without
walking an entire monorepo tree.

```bash
projects oss matrix \
  --root /home/hasna/Workspace/hasna/opensource \
  --prefix open- \
  --json
```

Each row includes the repo name/path, package name/version/bin metadata from
`package.json`, git branch/dirty/ahead/behind/remote state, tmux session/window
hints, latest task refs from `todos`, and latest pull request refs from `gh`
when those tools are available. The default limit is 25 repos, with an enforced
maximum of 200. Use `--limit`, `--no-prs`, `--no-tasks`, `--no-tmux`, and
`--timeout-ms` to keep routing scans fast in large workspaces or offline
contexts.

## Storage Sync

Set `HASNA_PROJECTS_DATABASE_URL` to your PostgreSQL connection string for
runtime or smoke commands, and do not print the value. `PROJECTS_DATABASE_URL`
remains available as a local/self-hosted fallback. This package ships no default
database, cluster, or secret-manager identifier — the connection string is
operator-supplied configuration only, loaded from your own secret store.

```bash
export HASNA_PROJECTS_DATABASE_URL="<your PostgreSQL connection string>"
```

`projects storage status --json` includes a `readiness` object that separates
the global registry sync target from local-only per-project `project.db` and
local asset directories. `readiness.cloudBackedRuntimeReady` is false until an
approval-backed migration adds Postgres tables/backfill for `project.db` data
and an S3 adapter/backfill for project asset files.

Before cutover, verify `projects storage status --json`, run a read-only smoke
against the canonical database, and keep legacy sources read-only until the
central rollback window closes.

`projects create --dry-run` is a true no-write creation plan. It returns planned DB writes, filesystem actions, tmux actions, verification steps, locks, and rollback records without writing rows or files. Creation cleanup remains available through MCP as `projects_cleanup_create`, removing only safe creation artifacts such as the project row, marker file, `.git`, and empty created directory.

## MCP Server

Add to an MCP client config:

```json
{
  "mcpServers": {
    "open-projects": {
      "command": "projects-mcp"
    }
  }
}
```

## HTTP mode

MCP uses stdio by default. A long-lived Streamable HTTP transport is also available on `127.0.0.1`:

```bash
projects-mcp --http              # default port 8871
MCP_HTTP=1 MCP_HTTP_PORT=8871 projects-mcp
```

Endpoints: `GET /health` → `{"status":"ok","name":"projects"}`, MCP at `POST/GET /mcp`.

### Project Tools

| Tool | Purpose |
| --- | --- |
| `projects_roots_list` / `projects_roots_add` / `projects_roots_show` / `projects_roots_update` / `projects_roots_delete` / `projects_roots_match` | Register, inspect, score, update, and delete root folders/path templates |
| `projects_recipes_list` / `projects_recipes_add` | Manage recipe defaults for project creation |
| `projects_agents_list` / `projects_agents_add` | Register human, CLI, service, and AI agents |
| `projects_tmux_profiles_list` / `projects_tmux_profiles_add` / `projects_tmux_profiles_apply` | Manage reusable tmux sessions/windows |
| `projects_list` / `projects_show` | Search and inspect projects |
| `projects_render_list` / `projects_render_show` / `projects_render_start` / `projects_render_status` / `projects_render_sessions` / `projects_render_roots` / `projects_render_recipes` | Emit validated JSON Render specs for project surfaces |
| `projects_store_inspect` | Inspect canonical project storage and the per-project app store under `$HASNA_PROJECTS_HOME/data/<workspace_id>/project.db` |
| `projects_canvases_list` / `projects_canvases_create` / `projects_canvases_upsert` / `projects_canvases_compose` / `projects_render_canvas` | Manage, compose, update, and render per-project React Flow canvas records |
| `projects_loops_link` / `projects_loops_list` | Link project stores to OpenLoops loops and summarize them through `@hasna/loops` |
| `projects_locations_list` / `projects_locations_add` | Inspect and register additional folder locations for a project |
| `projects_create` | Plan or create a project anywhere on disk |
| `projects_start` | Open or reuse a tmux session, ensure default `01`/`02` windows, and launch Codewith, Claude, OpenCode, Cursor, or no tool, with optional exact tmux windows |
| `projects_tmux_status` | Inspect expected and current tmux session/window status for a project |
| `projects_cleanup_create` | Clean up DB/files created by a project creation run using rollback records |
| `projects_import` / `projects_scan_local_roots` | Import existing folders as projects |
| `projects_import_github` / `projects_scan_roots` / `projects_sync_roots` | Import GitHub repos as local or remote-only projects, including configured GitHub root scans/syncs |
| `projects_github_publish` / `projects_github_unpublish` | Publish/unlink GitHub project metadata |
| `projects_link` | Merge external service IDs into project integrations |
| `projects_unlink` | Clear external service IDs from project integrations |
| `projects_update` | Update project metadata with audit events |
| `projects_tag` / `projects_untag` | Add or remove project tags without replacing the full tag list |
| `projects_archive` / `projects_unarchive` / `projects_delete` | Change project lifecycle status |
| `projects_doctor` | Validate markers, paths, locations, references, and failed runs |
| `projects_events_list` / `projects_event_record` | Inspect or record project audit events |
| `projects_lock` / `projects_unlock` / `projects_locks` | Coordinate project mutations |
| `projects_agent_eval` | Run prompt-agent eval cases and return success/confidence |
| `projects_agent_prompt` | Run the AI SDK/OpenRouter project agent loop |
| `projects_context` / `projects_next` / `projects_why` / `projects_handoff` | Agent-assist bundles: orientation, next-action suggestions, resolution trace, cross-agent handoff |
| `projects_runs_list` / `projects_runs_show` | Read the prompt-agent run ledger (list + full detail with tool-call trace) |

Workspace-named MCP aliases are removed from the public contract.

## Prompt Mode

Prompt mode uses AI SDK with OpenRouter. Configure the key with `OPENROUTER_API_KEY`, `PROJECTS_OPENROUTER_API_KEY`, or the local secrets vault. The default model is `openai/gpt-4o-mini`.

Mutations require `--yes`. Without approval, mutating tools return structured plans/previews. `--dry-run` forces no-write behavior even if `--yes` is present.

The prompt agent can inspect roots, recipes, agents, tmux profiles, and projects; create/update/tag/untag/archive/delete/import projects; import/publish GitHub repos; link/unlink external integrations; start projects with saved launch defaults; and plan/apply tmux profiles. It records agent runs and tool calls in SQLite.

`projects agent-eval` seeds temporary project fixtures into an isolated SQLite database under the eval base path and runs a repeatable prompt suite over root registration/matching, recipe and agent planning, project listing/show/events, create/deduplication, import/scan, update, archive/unarchive, delete/hard-delete, cleanup, verification, tmux planning, GitHub publish/unpublish/import, and integration linking. Live mode uses OpenRouter; `--mock` runs deterministic create-path coverage and skips live-only cases. The JSON summary reports `success_rate`, `confidence`, and `db_path`.

Normal `projects list` output hides prompt-agent eval fixtures. Use `projects list --include-evals` to inspect old fixtures and `projects cleanup-evals --dry-run --json` followed by `projects cleanup-evals --apply` to remove them.

## Compact output defaults

Human terminal output is compact by default to avoid filling agent context with
large records. List/history commands cap rows, truncate long text and paths, and
print a hint for the next detail command. Use `--limit <n>` to raise the row cap,
`--verbose` for extra table columns, full paths, and diagnostic checks,
`show`/`events list` for detail workflows, and `--json` for stable
machine-readable records.

Compact terminal defaults cover the noisy project registry commands plus smaller
registry lists such as `projects roots list`, `projects recipes list`,
`projects agents list`, `projects tmux-profiles list`, `projects locks`,
`projects locations list`, `projects budgets list`, and `projects doctor`.
Examples:

```bash
projects list --limit 25
projects roots list --limit 10 --verbose
projects doctor --limit 20 --verbose
projects events list my-app --limit 10 --verbose
```

MCP tools keep their existing full-record defaults for client compatibility.
Where supported, pass `compact: true` to receive compact summaries; compact MCP
calls accept `limit`, and `verbose: true` returns full records. Prompt-agent
tools use compact project/event summaries by default; agent tools expose
`verbose: true` for explicit detail retrieval.

## Data Model

Core internal tables:

- `roots`: named base folders, tags, path templates, default kind/recipe/tmux profile, GitHub defaults
- `recipes`: reusable creation metadata, variables, default tags, and scaffold steps
- `agents`: human, CLI, service, and AI actors with provider/model/permissions metadata
- `workspaces`: storage records backing projects with kind, status, root, recipe, path, tags, integrations, and metadata
- `workspace_locations`: machine-local paths for a project
- `workspace_events`: immutable project audit events for mutations and runtime actions
- `agent_runs`: prompt-loop run ledger with tool calls and results
- `tmux_profiles` / `tmux_profile_windows`: reusable tmux session/window templates
- `workspace_locks`: short-lived mutation locks
- `workspace_migration_map`: one-time legacy project-to-workspace mapping

Global registry DB path: `~/.hasna/projects/projects.db`

Per-project app data path: `~/.hasna/projects/data/<workspace_id>/project.db`

Cloud readiness contract: `docs/cloud-storage-readiness-contract.md`

Per-project app tables:

- `project_canvases`: React Flow-compatible dashboard/canvas records
- `project_data_models`: custom JSON data model definitions and render hints
- `project_data_records`: custom per-model JSON records
- `project_loop_links`: links to `@hasna/loops` loop ids/names

Global registry override: `HASNA_PROJECTS_DB_PATH`

Per-project app store root override: `HASNA_PROJECTS_HOME`

Projects home: `~/.hasna/projects`

Override: `HASNA_PROJECTS_HOME`

## SDK

The public SDK exports project-named functions and types. Storage-layer modules still use workspace names internally, but the package boundary is project-first.

```ts
import {
  createProject,
  updateProject,
  planProjectCreation,
  executeProjectCreation,
  runProjectAgentPrompt,
  startProject,
  ensureProjectStore,
  ensureDefaultProjectCanvas,
  listProjectCanvases,
  linkProjectLoop,
} from "@hasna/projects";
```

Per-project store helpers are also available from `@hasna/projects/project-store`.

## HTTP API (`projects-serve`) & self_hosted SDK

`projects-serve` is the self-hosted HTTP surface. It talks to cloud Postgres
directly (Amendment A1, pure-remote — no local cache or sync in the service).

```sh
# apply migrations, then serve
HASNA_PROJECTS_DATABASE_URL=postgres://… projects-serve migrate
HASNA_PROJECTS_DATABASE_URL=postgres://… HASNA_PROJECTS_API_SIGNING_KEY=… projects-serve   # :8080
```

Endpoints:

- `GET /health`, `GET /ready`, `GET /version` → `{status, version, mode}` (unauthenticated)
- `GET /openapi.json` → the OpenAPI 3.1 document
- `/v1/*` (API key required, header `x-api-key` or `Authorization: Bearer`):
  - `GET|POST /v1/projects`, `GET|PATCH|DELETE /v1/projects/{id}`,
    `POST /v1/projects/{id}/archive|unarchive`, `GET /v1/projects/{id}/events`
  - `GET|POST /v1/roots`, `GET|PATCH|DELETE /v1/roots/{id}`
  - `GET|POST /v1/agents`, `GET /v1/agents/{id}`
  - `GET|POST /v1/recipes`, `GET /v1/recipes/{id}`

Reads require the `projects:read` scope; writes require `projects:write`. Issue a
key with `contracts issue-key --app projects --scopes projects:*`.

The typed client is generated from the serve OpenAPI (`bun run sdk:generate`):

```ts
import { ProjectsClient, createProjectsClientFromEnv } from "@hasna/projects/sdk";

// self_hosted convention: PROJECTS_API_URL + PROJECTS_API_KEY (never a DSN)
const projects = createProjectsClientFromEnv();
const created = await projects.createProject({ name: "My Project", tags: ["demo"] });
const list = await projects.listProjects({ tag: "demo" });
```

## Architecture

```text
src/
├── cli/
│   ├── index.ts                 # project CLI and prompt entrypoint
│   └── commands/
│       ├── workspaces.ts         # project, roots, recipes, agents, tmux profiles
│       └── completion.ts         # project shell completion
├── db/
│   ├── database.ts               # SQLite init/path resolution
│   ├── project-store.ts          # per-project project.db helpers
│   ├── schema.ts                 # migrations
│   └── workspaces.ts             # workspace/root/recipe/agent/tmux services
├── lib/
│   ├── workspace-agent.ts        # AI SDK/OpenRouter prompt loop
│   ├── workspace-github.ts       # GitHub import/publish and integration linking
│   ├── workspace-plan.ts         # deterministic creation plans/executor
│   ├── workspace-runtime.ts      # directory/git/marker/tmux runtime actions
│   ├── workspace-import.ts       # arbitrary folder and registered-root import scanner
│   ├── workspace-migration.ts    # legacy migration dry-run, backup, and reports
│   └── workspace-doctor.ts       # marker/path/reference validation
├── mcp/
│   └── index.ts                  # project-first MCP server
├── project-store.ts              # per-project store SDK subpath exports
├── types/
│   └── workspace.ts              # internal storage/project domain types
└── index.ts                      # SDK exports
```

## License

Apache-2.0