@starlove/orbit 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +13 -0
- package/Dockerfile +34 -0
- package/README.md +216 -0
- package/SKILL-ORBIT.md +94 -0
- package/build.mjs +204 -0
- package/dist/full/apple-touch-icon.png +0 -0
- package/dist/full/cursors/live_arrow.cur +0 -0
- package/dist/full/cursors/live_link.cur +0 -0
- package/dist/full/cursors/live_move.cur +0 -0
- package/dist/full/cursors/live_select.cur +0 -0
- package/dist/full/cursors/live_unavail.cur +0 -0
- package/dist/full/favicon-32x32.png +0 -0
- package/dist/full/index.html +314 -0
- package/dist/full/js/app.70be7d06.js +554 -0
- package/dist/full/js/app.70be7d06.js.map +7 -0
- package/dist/full/orbits-favicon.png +0 -0
- package/dist/full/orbits-logo-bg.png +0 -0
- package/dist/full/orbits-logo.png +0 -0
- package/dist/full/styles.c2e3d20a.css +4892 -0
- package/dist/full/version.json +6 -0
- package/docs/AGENT_PROTOCOL.md +379 -0
- package/docs/MCP_SETUP.md +49 -0
- package/docs/mcp-client.example.json +13 -0
- package/package.json +62 -0
- package/pnpm-workspace.yaml +2 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/cursors/live_arrow.cur +0 -0
- package/public/cursors/live_link.cur +0 -0
- package/public/cursors/live_move.cur +0 -0
- package/public/cursors/live_select.cur +0 -0
- package/public/cursors/live_unavail.cur +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/index.html +314 -0
- package/public/js/api.js +35 -0
- package/public/js/app.js +35 -0
- package/public/js/board-menu.js +145 -0
- package/public/js/config.js +19 -0
- package/public/js/create-card.js +153 -0
- package/public/js/dom.js +19 -0
- package/public/js/drawer.js +137 -0
- package/public/js/format.js +108 -0
- package/public/js/kanban.js +475 -0
- package/public/js/local-backend.js +934 -0
- package/public/js/main.js +142 -0
- package/public/js/modal.js +31 -0
- package/public/js/router.js +114 -0
- package/public/js/search.js +53 -0
- package/public/js/settings-tabs.js +7 -0
- package/public/js/settings.js +931 -0
- package/public/js/sse.js +156 -0
- package/public/js/starscape.js +315 -0
- package/public/js/state.js +54 -0
- package/public/js/ticket-detail.js +766 -0
- package/public/js/toast.js +74 -0
- package/public/js/transport.js +22 -0
- package/public/js/unread.js +121 -0
- package/public/orbits-favicon.png +0 -0
- package/public/orbits-logo-bg.png +0 -0
- package/public/orbits-logo.png +0 -0
- package/public/styles.css +4892 -0
- package/src/cli/orbit.js +438 -0
- package/src/core/agent.js +262 -0
- package/src/core/auth.js +49 -0
- package/src/core/backups.js +136 -0
- package/src/core/board.js +112 -0
- package/src/core/boards.js +310 -0
- package/src/core/bootstrap.js +162 -0
- package/src/core/db.js +237 -0
- package/src/core/delete-board.js +53 -0
- package/src/core/events.js +314 -0
- package/src/core/http.js +81 -0
- package/src/core/paths.js +25 -0
- package/src/core/provision-repo-board.js +113 -0
- package/src/core/queries.js +246 -0
- package/src/core/registry.js +207 -0
- package/src/core/relations.js +163 -0
- package/src/core/router.js +414 -0
- package/src/core/search.js +70 -0
- package/src/core/seed.js +226 -0
- package/src/core/snapshots.js +255 -0
- package/src/core/states.js +167 -0
- package/src/core/system-picker.js +93 -0
- package/src/core/tickets.js +344 -0
- package/src/core/util.js +105 -0
- package/src/mcp-server.js +773 -0
- package/src/server.js +48 -0
package/.dockerignore
ADDED
package/Dockerfile
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
|
|
3
|
+
FROM node:22-slim AS build
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
|
|
6
|
+
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
|
7
|
+
|
|
8
|
+
RUN corepack enable && corepack prepare pnpm@10.20.0 --activate
|
|
9
|
+
|
|
10
|
+
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
|
11
|
+
RUN pnpm install --frozen-lockfile
|
|
12
|
+
|
|
13
|
+
COPY build.mjs ./
|
|
14
|
+
COPY public ./public
|
|
15
|
+
RUN pnpm run build:full
|
|
16
|
+
|
|
17
|
+
FROM node:22-slim AS runtime
|
|
18
|
+
WORKDIR /app
|
|
19
|
+
|
|
20
|
+
ENV NODE_ENV=production \
|
|
21
|
+
PORT=3337 \
|
|
22
|
+
DATA_DIR=/data
|
|
23
|
+
|
|
24
|
+
COPY package.json ./
|
|
25
|
+
COPY SKILL-ORBIT.md ./SKILL-ORBIT.md
|
|
26
|
+
COPY docs ./docs
|
|
27
|
+
COPY src ./src
|
|
28
|
+
COPY --from=build /app/dist/full ./dist/full
|
|
29
|
+
|
|
30
|
+
VOLUME ["/data"]
|
|
31
|
+
EXPOSE 3337
|
|
32
|
+
|
|
33
|
+
ENTRYPOINT ["node", "/app/src/cli/orbit.js"]
|
|
34
|
+
CMD ["serve", "--cwd", "/workspace"]
|
package/README.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Starscape Orbit
|
|
2
|
+
|
|
3
|
+
*Spin up an Orbit to track the motion of your project.*
|
|
4
|
+
|
|
5
|
+
**Orbit** by ***Starscape*** is a lightweight, kanban-style project management solution for human-agent collaboration.
|
|
6
|
+
|
|
7
|
+
Use it when you want the board to live with the code: one SQLite file, one small service, one human UI, and one agent-first API.
|
|
8
|
+
|
|
9
|
+
- Human UI for creating, editing, and moving tickets kanban-style.
|
|
10
|
+
- Agent API/MCP for claiming work, reading context, updating implementation notes, commenting, and writing project memory.
|
|
11
|
+
|
|
12
|
+
## 🛰️ Zero runtime dependencies
|
|
13
|
+
|
|
14
|
+
**Orbit installs with zero third-party runtime dependencies.** The server, CLI, and MCP layer use Node.js built-ins: `node:sqlite`, `node:http`, `node:crypto`, and friends.
|
|
15
|
+
|
|
16
|
+
The single dev-only dependency is [`esbuild`](https://esbuild.github.io/), used to bundle the web UI at build time. It never ships and is not needed to run the server, CLI, or MCP.
|
|
17
|
+
|
|
18
|
+
## Install and use Orbit
|
|
19
|
+
|
|
20
|
+
Install the CLI:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
corepack enable
|
|
24
|
+
pnpm add -g @starlove/orbit
|
|
25
|
+
|
|
26
|
+
# or, from source:
|
|
27
|
+
git clone git@github.com:brucestarlove/ss-orbit.git
|
|
28
|
+
cd ss-orbit
|
|
29
|
+
pnpm install
|
|
30
|
+
pnpm run build:full
|
|
31
|
+
pnpm link --global
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
From the repo you want a board for:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
cd path/to/your-app
|
|
38
|
+
orbit init # creates .orbit/board.db + SKILL-ORBIT.md + AGENTS.md
|
|
39
|
+
orbit init --example # optional: add onboarding example tickets
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Start one runtime and keep using it:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
orbit serve # host runtime, uses ~/.orbit
|
|
46
|
+
# or
|
|
47
|
+
orbit docker # Docker runtime, uses <repo>/.orbit/docker-data
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Both serve `http://localhost:3337`. Use `--port 3400` to change ports and `--cwd path/to/repo` to bind a specific repo.
|
|
51
|
+
|
|
52
|
+
`orbit serve` and `orbit docker` run the same app but use different registries by default. A board registered in one mode will not appear in the other unless you share `DATA_DIR`, re-register it, or import a snapshot.
|
|
53
|
+
|
|
54
|
+
For multiple boards, run your chosen runtime once. In another terminal, `cd` into another repo and run `orbit init`; the board joins that runtime's board picker without a restart.
|
|
55
|
+
|
|
56
|
+
Docker options:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
orbit docker # builds starscape-orbit:local, then serves http://localhost:3337
|
|
60
|
+
orbit docker --port 3400 # same container flow on a different port
|
|
61
|
+
orbit docker --no-build # reuse an existing starscape-orbit:local image
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`orbit docker` mounts the selected project into the container. Board data still lives in `.orbit/board.db`; Docker-only registry, exports, and backups live under `<repo>/.orbit/docker-data` by default.
|
|
65
|
+
|
|
66
|
+
Wipe a single board and start over with `orbit reset` (deletes `.orbit/`, `SKILL-ORBIT.md`, and the registry row), then `orbit init` again.
|
|
67
|
+
|
|
68
|
+
## Use with AI
|
|
69
|
+
|
|
70
|
+
Orbit ships with everything an agent needs to claim work, read tickets, and write back implementation notes — but it stays out of your way until you turn it on.
|
|
71
|
+
|
|
72
|
+
1. **Enable AI on the board.** Open Settings (gear icon) → **AI** tab → toggle **Enable AI**. Orbit provisions the `AI Ready`, `In Progress`, and `Review` lanes if they're missing, surfaces the agent-context fields, and reveals the MCP setup snippet.
|
|
73
|
+
2. **Fill in agent context.** Settings → AI → *Agent Instructions* is the project-level briefing every agent reads before touching a ticket. Describe what the project is, who it serves, the stack, and any rules of the road.
|
|
74
|
+
3. **Register the MCP server with your agent.** The AI tab generates copy-pasteable snippets for Claude Code, Cursor, Codex, OpenCode, OpenClaw, and other MCP-capable clients — pick your OS + client and paste the command into your agent's MCP config.
|
|
75
|
+
4. **Restart the agent.** On its next boot it discovers the Orbit MCP tools (`board_claim_next`, `board_get_ticket_context`, `board_update_ticket`, etc.).
|
|
76
|
+
5. **Point the agent at a ticket.** From inside the repo, ask the agent to read `SKILL-ORBIT.md` and then work on a ticket via the Orbit MCP — e.g., *"Use the Orbit MCP to claim the next AI-ready card and start work."* It pulls a focused context pack, claims the ticket, and updates the board as it goes.
|
|
77
|
+
|
|
78
|
+
If you initialized with `--example`, ticket #12 (`Try Orbit MCP on this ticket`) is a good first exercise once MCP is connected.
|
|
79
|
+
|
|
80
|
+
## Vocabulary
|
|
81
|
+
|
|
82
|
+
- **Board** — one repo's planning surface. Each board carries its own `agent_instructions`, lanes, tickets, and memory entries.
|
|
83
|
+
- **Lane** — a column. Lanes are user-defined and freely named/reordered. The seed gives you `Backlog`, `Todo`, `In Progress`, `Review`, `Done`, `Cancelled`; turning on AI also pins `AI Ready`, `In Progress`, and `Review` as anchor lanes the agent flow keys off.
|
|
84
|
+
- **Ticket types**:
|
|
85
|
+
- `epic` — index card for a multi-feature initiative. Owns child tickets via `parent_ticket_id`.
|
|
86
|
+
- `feature` — a standalone capability or new behavior.
|
|
87
|
+
- `task` — a smaller, scoped unit of work.
|
|
88
|
+
- `bug` — defect, regression, or broken behavior.
|
|
89
|
+
- **Checkpoint** — mid-work human judgment: call `checkpoint` with your question. The ticket moves to `Review`; the `checkpoint` comment marks the blocking fork. Use `complete` when work is finished.
|
|
90
|
+
- **Two relationship models** — hierarchy (`parent_ticket_id`, set on the row, only epics own children) is separate from the relations table (`relates_to`, `blocks`, `blocked_by`). The server keeps them disjoint.
|
|
91
|
+
- **Blockers** — a per-card `blocked_by` row in the relations table is the single source of truth. If the row exists, the ticket is blocked. Rows are removed two ways: the user deletes the relation manually in the UI, **or** the *blocking* ticket lands in the **Done** lane and the server auto-deletes every relation row that named it as a blocker. (Epic targets expand to their open children — see `GET /api/tickets/:id/blockers`.)
|
|
92
|
+
|
|
93
|
+
## Requirements
|
|
94
|
+
|
|
95
|
+
- **Node.js 22+**. The server uses the built-in `node:sqlite` module (an experimental Node feature). On boot you'll see a Node experimental-feature warning — that's expected.
|
|
96
|
+
- **pnpm 10+ for development**. The repo pins pnpm through `packageManager`; use Corepack (`corepack enable`) to get the right version.
|
|
97
|
+
|
|
98
|
+
Bun is not used as the Orbit runtime because Orbit depends on Node's built-in `node:sqlite` module.
|
|
99
|
+
|
|
100
|
+
## How it works
|
|
101
|
+
|
|
102
|
+
Each repo gets its own board database. Orbit keeps a small central registry that maps repo paths to their board files; the CLI, web app, and MCP server use explicit project roots when provided, then fall back to walking upward to find `.orbit/board.db`.
|
|
103
|
+
|
|
104
|
+
- **Per-repo board** — `.orbit/board.db` lives in the repo it tracks. `orbit init` creates it, drops `SKILL-ORBIT.md` at the repo root, and creates/updates `AGENTS.md` with a terse pointer so agents load the full Orbit protocol only when relevant. Add `--example` to create onboarding cards.
|
|
105
|
+
- **Central registry** — `registry.db` records each board's path, slug, and last-active timestamp. Host `orbit serve` uses `~/.orbit`; `orbit docker` uses `<repo>/.orbit/docker-data`. Keep one mode unless you intentionally share `DATA_DIR`, re-register boards, or import snapshots.
|
|
106
|
+
- **Project-root discovery** — `orbit serve`, `orbit mcp`, and MCP-attached agents resolve a board from an explicit `--cwd` / `PROJECT_ROOT` first, then fall back to walking up from process cwd to find `.orbit/board.db`. Persistent MCP configs should use an explicit root so the right board attaches even when the agent launches elsewhere.
|
|
107
|
+
- **Two channels for AI** — `AGENTS.md` is the auto-loaded briefing, with an Orbit-managed pointer to `SKILL-ORBIT.md`; the *MCP config* tells the agent runtime how to launch Orbit's MCP server for the right project. The briefing travels with the repo; the MCP registration is per-agent-install.
|
|
108
|
+
- **Snapshot-portable** — Settings → Repository → Export downloads a `.orbit.json` snapshot. Import on another install (or in the browser preview at [orbit.starscape.app/app](https://orbit.starscape.app/app)) to restore the same board.
|
|
109
|
+
|
|
110
|
+
## Developing Orbit
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
pnpm install
|
|
114
|
+
pnpm run dev
|
|
115
|
+
pnpm test
|
|
116
|
+
pnpm run build
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Open `http://localhost:3337`.
|
|
120
|
+
|
|
121
|
+
If you want an AI agent to work this board, point it at [SKILL-ORBIT.md](SKILL-ORBIT.md). Agent-facing HTTP/MCP details live in [docs/AGENT_PROTOCOL.md](docs/AGENT_PROTOCOL.md).
|
|
122
|
+
|
|
123
|
+
**Two channels:** MCP config tells your AI app where `mcp-server.js` lives. `SKILL-ORBIT.md` is the repo-local briefing the agent reads before working.
|
|
124
|
+
|
|
125
|
+
## Typical Workflow
|
|
126
|
+
|
|
127
|
+
A normal day on a shared board:
|
|
128
|
+
|
|
129
|
+
1. **Human** creates an epic for a new initiative and fills in card description with the high-level breakdown.
|
|
130
|
+
2. **Human** or **Agent** breaks it into `feature` / `task` / `bug` cards under the epic and moves the workable ones to the `AI Ready` lane.
|
|
131
|
+
3. **Human** directs **Agent** to find a ticket by number or name, or tell it to call `claim-next` to get one from the `AI Ready` column, which gets context for the ticket and important project notes, and starts work. Blocked tickets are skipped automatically.
|
|
132
|
+
4. **Agent** either:
|
|
133
|
+
- calls `complete` with a summary and PR link, moving the card to `Review`; or
|
|
134
|
+
- calls `checkpoint` with a question if it is blocked mid-flight. Orbit moves the card to `Review` and records a checkpoint comment.
|
|
135
|
+
5. **Human** reviews, merges, and moves the card to the `Done` lane (or sends it back with comments).
|
|
136
|
+
|
|
137
|
+
## MCP
|
|
138
|
+
|
|
139
|
+
Orbit ships a stdio MCP server for agents that prefer tools over raw HTTP. You don't normally start it yourself; your agent runtime spawns it from the snippet in **Settings → AI**. To run it standalone:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
orbit mcp
|
|
143
|
+
# equivalent: node src/mcp-server.js
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
MCP uses the same registry and board databases when launched with the same `DATA_DIR` and `--cwd` as the web runtime.
|
|
147
|
+
|
|
148
|
+
A copy-editable client config lives at [docs/mcp-client.example.json](docs/mcp-client.example.json). The web app also generates setup snippets from **Settings → AI**. More detail is in [docs/MCP_SETUP.md](docs/MCP_SETUP.md).
|
|
149
|
+
|
|
150
|
+
### MCP tools
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
| Purpose | Tool |
|
|
154
|
+
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
155
|
+
| Index | `board_list` (tiny registry), `board_set_active` |
|
|
156
|
+
| Board context | `board_context` (settings + journal + deployment; optional board + `include_struck`) |
|
|
157
|
+
| Read | `board_read_ticket`, `board_read_comments`, `board_get_ticket_context` (heavy), `board_get_ticket_relations`, `board_get_ticket_blockers`, `board_search`, `board_list_archive` |
|
|
158
|
+
| Agent flow | `board_claim_next`, `board_checkpoint`, `board_complete` |
|
|
159
|
+
| Write | `board_update_ticket`, `board_add_comment`, `board_add_board_entry`, `board_archive_ticket`, `board_restore_ticket`, `board_delete_ticket` |
|
|
160
|
+
| Admin | `board_export_board`, `board_update_settings` |
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
## First-Run Seed
|
|
164
|
+
|
|
165
|
+
`orbit init` (or first server start in a repo without a board) creates a universal board seed in the new `.orbit/board.db`:
|
|
166
|
+
|
|
167
|
+
- **Board**: name from `BOARD_NAME` env var, otherwise inferred from `package.json` `name` or the repo folder name. Slug from `BOARD_SLUG` or derived from the name.
|
|
168
|
+
- **Repo metadata**: `REPO_URL` env var or `git config --get remote.origin.url`; `SYSTEM_PATH` env var or the repo path passed to `orbit init`.
|
|
169
|
+
- **Default branch**: `DEFAULT_BRANCH` env var, otherwise the current git branch.
|
|
170
|
+
- **Lanes (6)**: `Backlog`, `Todo` (default), `In Progress`, `Review`, `Done`, `Cancelled`.
|
|
171
|
+
- **Labels (7)**: `human-only`, `needs-human-input`, `needs-decomposition`, `needs-followup`, `tech-debt`, `security`, `onboarding`.
|
|
172
|
+
- **Onboarding cards (optional)**: pass `--example` to create an epic `#1` plus children `#2`, `#3`, and `#12 — Try Orbit MCP on this ticket`.
|
|
173
|
+
- **Free-form fields**: `project_notes` (Notes tab) and `agent_instructions` (AI tab) get placeholder text you should replace.
|
|
174
|
+
|
|
175
|
+
Set env vars before `orbit init` or first runtime start to skip the placeholders:
|
|
176
|
+
|
|
177
|
+
```powershell
|
|
178
|
+
$env:BOARD_NAME="My App"; $env:BOARD_SLUG="my-app"; $env:REPO_URL="https://github.com/you/my-app"; orbit init
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
BOARD_NAME="My App" BOARD_SLUG=my-app REPO_URL=https://github.com/you/my-app orbit init
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Troubleshooting
|
|
186
|
+
|
|
187
|
+
- **UI loads but lanes are blank** — usually a cached `app.js`. Hard-refresh the page (Ctrl+Shift+R) and check the browser console.
|
|
188
|
+
- **`node:sqlite` is not a known module** — you're on Node < 22. Upgrade to Node 22+.
|
|
189
|
+
- **Port 3337 already in use** — run `orbit serve --port 3340` or `orbit docker --port 3340`.
|
|
190
|
+
|
|
191
|
+
## Data
|
|
192
|
+
|
|
193
|
+
Orbit stores each board in the repo it belongs to:
|
|
194
|
+
|
|
195
|
+
- Board data: `.orbit/board.db`
|
|
196
|
+
- Host registry and backups: `~/.orbit` by default
|
|
197
|
+
- Docker registry and backups: `<repo>/.orbit/docker-data` by default
|
|
198
|
+
|
|
199
|
+
Override runtime data with:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
DATA_DIR=/path/to/data orbit serve
|
|
203
|
+
orbit docker --data-dir /path/to/data
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
```powershell
|
|
207
|
+
$env:DATA_DIR="C:\path\to\data"; orbit serve
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
The UI exposes **Export** and **Import** from **Settings → Repository**.
|
|
211
|
+
|
|
212
|
+
**Export** downloads a portable `.orbit.json` file you can move to another computer or import later.
|
|
213
|
+
|
|
214
|
+
**Automatic backups** are separate from Export. Orbit quietly saves local backup copies after you make changes, waiting about two minutes after your last edit so it does not create a file for every click. Orbit also makes an immediate backup before risky actions like deleting a board, resetting Orbit, or replacing a board during import.
|
|
215
|
+
|
|
216
|
+
Backups are stored under the active runtime data dir's `backups/` folder. They are for local recovery; Export is still the easiest way to intentionally save or share a board.
|
package/SKILL-ORBIT.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# SKILL-ORBIT.md
|
|
2
|
+
|
|
3
|
+
Read this before repo work. This board is the repo-local memory and execution layer.
|
|
4
|
+
|
|
5
|
+
## MCP, cwd, and this file
|
|
6
|
+
|
|
7
|
+
These are separate concerns; mixing them causes confusion:
|
|
8
|
+
|
|
9
|
+
- **Where Orbit is installed** — Your MCP client stores an **absolute path** to Orbit's `src/mcp-server.js` (the generated snippet under **Settings → AI**). Claude Code / Codex / Cursor keep that registration. Starting your agent **from another folder** does not change that path.
|
|
10
|
+
|
|
11
|
+
- **Which board MCP tools use** — The MCP helper uses the explicit project root passed by `orbit mcp --cwd <repo>` or `PROJECT_ROOT`; without either, it falls back to the process current working directory and walks upward until it finds a repo with `.orbit/board.db` (details in [`docs/MCP_SETUP.md`](docs/MCP_SETUP.md)). Prefer explicit `--cwd` / `PROJECT_ROOT` in persistent MCP configs so the right board attaches even when the agent launches elsewhere.
|
|
12
|
+
|
|
13
|
+
- **This file** — Coding agents resolve `SKILL-ORBIT.md` through normal **filesystem / workspace search**, not through MCP tools. Keep a copy **in each repo** where you run an agent against that board—usually the repo that contains `.orbit/board.db`. If you edit in a multi-root workspace with more than one `SKILL-ORBIT.md`, name the repo or path explicitly in your prompt.
|
|
14
|
+
|
|
15
|
+
## Use The Board
|
|
16
|
+
|
|
17
|
+
- Read `agent_instructions` on the board first, then the ticket context pack.
|
|
18
|
+
- Respect blockers and parent epics.
|
|
19
|
+
- Use comments for discussion and breadcrumbs.
|
|
20
|
+
- Use ticket fields for durable implementation records: `ai_plan`, `implementation_summary`, `implementation_updates`.
|
|
21
|
+
- Add a project entry when the context should outlive one ticket.
|
|
22
|
+
|
|
23
|
+
## Historical Backfill
|
|
24
|
+
|
|
25
|
+
If asked to create historical tickets from logs, docs, commit history, notes, or conversations, reconstruct planning structure instead of making a flat list.
|
|
26
|
+
|
|
27
|
+
- `epic`: multi-phase chunk or large initiative.
|
|
28
|
+
- `feature`: standalone capability or one coherent stage of an epic.
|
|
29
|
+
- `task`: smaller scoped unit of work, including maintenance, cleanup, config, refactor, or repo hygiene.
|
|
30
|
+
- `bug`: defect, regression, incident, or broken behavior.
|
|
31
|
+
|
|
32
|
+
Rules:
|
|
33
|
+
|
|
34
|
+
- Prefer fewer, clearer epics with named feature cards underneath.
|
|
35
|
+
- Preserve source references in descriptions or comments.
|
|
36
|
+
- Mark uncertainty with `Inferred from...` or `Source suggests...`.
|
|
37
|
+
- Put implementation facts in `implementation_summary`.
|
|
38
|
+
- Put chronology, pitfalls, and lessons in `implementation_updates`.
|
|
39
|
+
- Keep future work open unless the source clearly supports `Done`.
|
|
40
|
+
- Do not invent acceptance criteria.
|
|
41
|
+
|
|
42
|
+
## Board Context Model
|
|
43
|
+
|
|
44
|
+
The board is the unit of organization. Each board is tied to one repo and carries:
|
|
45
|
+
|
|
46
|
+
- `repo_url`, `system_path`, `default_branch`: where the code lives.
|
|
47
|
+
- `agent_instructions`: project-level context for agents (purpose, surface area, stack, constraints, operating rules — included in ticket context packs). Edited under Settings → AI.
|
|
48
|
+
- `project_notes`: Notes For You — personal reminders on the board (Settings → Notes).
|
|
49
|
+
|
|
50
|
+
Use board-level memory entries for durable history:
|
|
51
|
+
|
|
52
|
+
- `decision`: UX or architecture choices agents should follow instead of defaulting to generic codegen.
|
|
53
|
+
- `lesson`: mistakes or discoveries distilled as "do X instead of Y when working with Z."
|
|
54
|
+
|
|
55
|
+
## Ticket Relationships
|
|
56
|
+
|
|
57
|
+
Two distinct, non-overlapping mechanisms:
|
|
58
|
+
|
|
59
|
+
- **Hierarchy** — `parent_ticket_id` on the ticket row. One epic owns many features/tasks/bugs. Set on create or via `PATCH /api/tickets/:id`. Only epics can have children; pass `null` to detach. Do not duplicate this link in `relations`.
|
|
60
|
+
- **Relations** — rows in the `relations` table with `type` of `relates_to`, `blocks`, or `blocked_by`. Use these for cross-cutting connections that aren't ownership: another team's ticket relates to mine, ticket A blocks ticket B, etc. The board rejects relations between a ticket and its direct parent epic (`relation_redundant_with_parent`) and self-relations (`relation_self`).
|
|
61
|
+
|
|
62
|
+
Before working a ticket, check blockers. A ticket is **not workable** while it has any `blocked_by` relation row. The relation row is the source of truth — it gets removed when the blocking ticket moves into the Done lane (state role `done`, server auto-deletes) or when a user removes the link manually in the UI. If a `blocked_by` target is an epic, the rule expands to *the epic's open children* — the ticket only becomes workable once every child sits in a Done-role lane (or is archived). The `claim-next` scheduler skips blocked tickets automatically; for ad-hoc checks call `GET /api/tickets/:id/blockers` (returns `can_start` + the unresolved blockers list, with `via_epic_id` on entries that came from epic expansion).
|
|
63
|
+
|
|
64
|
+
For a full picture of cross-cutting links call `GET /api/tickets/:id/relations`. The context pack (`GET /api/tickets/:id/context`) already includes both relations and blockers, so this dedicated endpoint is mostly for status-style queries that don't need the full pack.
|
|
65
|
+
|
|
66
|
+
## Common API Actions
|
|
67
|
+
|
|
68
|
+
- `GET /api/boards/:board_id/context`
|
|
69
|
+
- `GET /api/boards/:board_id/archive`
|
|
70
|
+
- `GET /api/tickets/:ticket_id/context?depth=1`
|
|
71
|
+
- `GET /api/tickets/:ticket_id/relations`
|
|
72
|
+
- `GET /api/tickets/:ticket_id/blockers`
|
|
73
|
+
- `POST /api/agent/claim-next`
|
|
74
|
+
- `PATCH /api/tickets/:ticket_id`
|
|
75
|
+
- `POST /api/tickets/:ticket_id/archive`
|
|
76
|
+
- `POST /api/tickets/:ticket_id/restore`
|
|
77
|
+
- `DELETE /api/tickets/:ticket_id`
|
|
78
|
+
- `POST /api/relations` / `DELETE /api/relations/:id`
|
|
79
|
+
- `POST /api/boards/:board_id/entries`
|
|
80
|
+
|
|
81
|
+
## Archive and Delete
|
|
82
|
+
|
|
83
|
+
Removing a card is a two-step process:
|
|
84
|
+
|
|
85
|
+
1. **Archive** (`POST /api/tickets/:ticket_id/archive`) — soft-deletes the card. Archived tickets are excluded from `bootstrap`, `search`, `claim-next`, blockers, relations, and the kanban view, but their data and comments are preserved. Reversible via `POST /api/tickets/:ticket_id/restore`.
|
|
86
|
+
2. **Delete** (`DELETE /api/tickets/:ticket_id`) — permanently removes the card. The ticket MUST already be archived; calling DELETE on a live card returns `409 ticket_not_archived`. Comments, labels, and relations are removed via cascade. Events for the ticket are kept but their `ticket_id` becomes `NULL`.
|
|
87
|
+
|
|
88
|
+
`GET /api/boards/:board_id/archive` returns `{ tickets: [...] }` listing archived tickets ordered by archive time (most recent first).
|
|
89
|
+
|
|
90
|
+
The same flow is exposed over MCP as `board_archive_ticket`, `board_restore_ticket`, `board_delete_ticket`, and `board_list_archive`. Always archive before deleting; archive when in doubt — restore is cheap, deletion is not.
|
|
91
|
+
|
|
92
|
+
## Current Rule
|
|
93
|
+
|
|
94
|
+
Board is canonical for planning state. Git repo is canonical for code.
|
package/build.mjs
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Two-edition static build. Bundles public/js/main.js with esbuild, copies
|
|
3
|
+
// HTML/CSS/images alongside, and rewrites the script tag to point at the
|
|
4
|
+
// hashed bundle. Edition is selected via `--edition=full|preview`; preview
|
|
5
|
+
// substitutes `__ORBIT_EDITION__` so config.js reports edition="preview".
|
|
6
|
+
|
|
7
|
+
import { build } from "esbuild";
|
|
8
|
+
import {
|
|
9
|
+
cpSync,
|
|
10
|
+
existsSync,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
readdirSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
statSync,
|
|
16
|
+
writeFileSync
|
|
17
|
+
} from "node:fs";
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
import { extname, join, resolve } from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
|
|
22
|
+
const ROOT = resolve(fileURLToPath(import.meta.url), "..");
|
|
23
|
+
const PUBLIC_DIR = join(ROOT, "public");
|
|
24
|
+
const DIST_DIR = join(ROOT, "dist");
|
|
25
|
+
|
|
26
|
+
function parseArgs(argv) {
|
|
27
|
+
const args = { edition: "full" };
|
|
28
|
+
for (const a of argv.slice(2)) {
|
|
29
|
+
if (a.startsWith("--edition=")) args.edition = a.slice("--edition=".length);
|
|
30
|
+
}
|
|
31
|
+
if (!["full", "preview", "site"].includes(args.edition)) {
|
|
32
|
+
throw new Error(`--edition must be "full", "preview", or "site" (got "${args.edition}")`);
|
|
33
|
+
}
|
|
34
|
+
return args;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// "site" output combines the marketing landing at / with the preview app
|
|
38
|
+
// at /app/ — the layout Vercel deploys for orbit.starscape.app.
|
|
39
|
+
function assembleSite() {
|
|
40
|
+
const previewDir = join(DIST_DIR, "preview");
|
|
41
|
+
const siteDir = join(DIST_DIR, "site");
|
|
42
|
+
if (!existsSync(previewDir)) {
|
|
43
|
+
throw new Error("dist/preview missing — run `npm run build:preview` first");
|
|
44
|
+
}
|
|
45
|
+
rmSync(siteDir, { recursive: true, force: true });
|
|
46
|
+
mkdirSync(siteDir, { recursive: true });
|
|
47
|
+
|
|
48
|
+
// Marketing assets live at the repo root (the landing page references
|
|
49
|
+
// public/orbits-logo.png etc., which we mirror under public/ at the site
|
|
50
|
+
// root so existing href values keep resolving).
|
|
51
|
+
const marketingHtml = readFileSync(join(ROOT, "index.html"), "utf8")
|
|
52
|
+
.replace(/href="public\/index\.html"/g, 'href="/app/"');
|
|
53
|
+
writeFileSync(join(siteDir, "index.html"), marketingHtml);
|
|
54
|
+
writeFileSync(join(siteDir, "install.html"), marketingHtml);
|
|
55
|
+
writeFileSync(join(siteDir, "support.html"), marketingHtml);
|
|
56
|
+
|
|
57
|
+
for (const file of ["apple-touch-icon.png", "favicon-32x32.png", "orbits-favicon.png"]) {
|
|
58
|
+
const src = join(ROOT, file);
|
|
59
|
+
if (existsSync(src)) cpSync(src, join(siteDir, file));
|
|
60
|
+
}
|
|
61
|
+
const faviconSrc = join(ROOT, "favicon-32x32.png");
|
|
62
|
+
if (existsSync(faviconSrc)) cpSync(faviconSrc, join(siteDir, "favicon.ico"));
|
|
63
|
+
|
|
64
|
+
// Marketing references public/orbits-logo.png etc. — mirror the public/
|
|
65
|
+
// image set at the site root so we don't have to rewrite every img src.
|
|
66
|
+
mkdirSync(join(siteDir, "public"), { recursive: true });
|
|
67
|
+
for (const entry of readdirSync(join(ROOT, "public"))) {
|
|
68
|
+
if (!entry.endsWith(".png")) continue;
|
|
69
|
+
cpSync(join(ROOT, "public", entry), join(siteDir, "public", entry));
|
|
70
|
+
}
|
|
71
|
+
const cursorsSrc = join(ROOT, "public", "cursors");
|
|
72
|
+
if (existsSync(cursorsSrc)) {
|
|
73
|
+
cpSync(cursorsSrc, join(siteDir, "public", "cursors"), { recursive: true });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Preview app at /app/.
|
|
77
|
+
cpSync(previewDir, join(siteDir, "app"), { recursive: true });
|
|
78
|
+
const appIndexPath = join(siteDir, "app", "index.html");
|
|
79
|
+
let appHtml = readFileSync(appIndexPath, "utf8");
|
|
80
|
+
appHtml = appHtml
|
|
81
|
+
.replace(/href="\.\/(apple-touch-icon\.png|favicon-32x32\.png)"/g, 'href="/app/$1"')
|
|
82
|
+
.replace(/href="(styles\.[^"]+\.css)"/g, 'href="/app/$1"')
|
|
83
|
+
.replace(/src="\.\/([^"]+\.png)"/g, 'src="/app/$1"')
|
|
84
|
+
.replace(/src="(js\/app\.[^"]+\.js)"/g, 'src="/app/$1"');
|
|
85
|
+
writeFileSync(appIndexPath, appHtml);
|
|
86
|
+
|
|
87
|
+
console.log(`✓ site → ${siteDir}`);
|
|
88
|
+
console.log(` / marketing landing`);
|
|
89
|
+
console.log(` /app/ preview kanban (IndexedDB)`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function hashContent(buffer) {
|
|
94
|
+
return createHash("sha256").update(buffer).digest("hex").slice(0, 8);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Preview build replaces api.js's transport import with the IndexedDB
|
|
98
|
+
// backend. Filter matches the literal `./transport.js` import string only —
|
|
99
|
+
// other imports keep their normal resolution.
|
|
100
|
+
function previewTransportPlugin() {
|
|
101
|
+
return {
|
|
102
|
+
name: "preview-transport",
|
|
103
|
+
setup(build) {
|
|
104
|
+
build.onResolve({ filter: /^\.\/transport\.js$/ }, (args) => ({
|
|
105
|
+
path: resolve(args.resolveDir, "local-backend.js")
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function run() {
|
|
112
|
+
const { edition } = parseArgs(process.argv);
|
|
113
|
+
if (edition === "site") {
|
|
114
|
+
return assembleSite();
|
|
115
|
+
}
|
|
116
|
+
const outDir = join(DIST_DIR, edition);
|
|
117
|
+
|
|
118
|
+
rmSync(outDir, { recursive: true, force: true });
|
|
119
|
+
mkdirSync(outDir, { recursive: true });
|
|
120
|
+
|
|
121
|
+
// 1. Bundle JS. Single entry → single file, ESM, minified, tree-shaken.
|
|
122
|
+
const bundleResult = await build({
|
|
123
|
+
entryPoints: [join(PUBLIC_DIR, "js", "main.js")],
|
|
124
|
+
bundle: true,
|
|
125
|
+
format: "esm",
|
|
126
|
+
target: "es2022",
|
|
127
|
+
minify: true,
|
|
128
|
+
sourcemap: true,
|
|
129
|
+
write: false,
|
|
130
|
+
outfile: "app.js",
|
|
131
|
+
define: {
|
|
132
|
+
__ORBIT_EDITION__: JSON.stringify(edition)
|
|
133
|
+
},
|
|
134
|
+
legalComments: "none",
|
|
135
|
+
plugins: edition === "preview" ? [previewTransportPlugin()] : []
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const jsFile = bundleResult.outputFiles.find((f) => f.path.endsWith(".js") && !f.path.endsWith(".map"));
|
|
139
|
+
const mapFile = bundleResult.outputFiles.find((f) => f.path.endsWith(".map"));
|
|
140
|
+
const jsHash = hashContent(jsFile.contents);
|
|
141
|
+
const jsName = `app.${jsHash}.js`;
|
|
142
|
+
mkdirSync(join(outDir, "js"), { recursive: true });
|
|
143
|
+
writeFileSync(join(outDir, "js", jsName), jsFile.contents);
|
|
144
|
+
if (mapFile) writeFileSync(join(outDir, "js", `${jsName}.map`), mapFile.contents);
|
|
145
|
+
|
|
146
|
+
// 2. Hash + copy CSS.
|
|
147
|
+
const cssSrc = join(PUBLIC_DIR, "styles.css");
|
|
148
|
+
const cssBuf = readFileSync(cssSrc);
|
|
149
|
+
const cssHash = hashContent(cssBuf);
|
|
150
|
+
const cssName = `styles.${cssHash}.css`;
|
|
151
|
+
writeFileSync(join(outDir, cssName), cssBuf);
|
|
152
|
+
|
|
153
|
+
// 3. Copy static assets (images, favicons) — anything in public/ that
|
|
154
|
+
// isn't HTML/CSS/JS source. Hashing not necessary; long-cache via headers.
|
|
155
|
+
for (const entry of readdirSync(PUBLIC_DIR)) {
|
|
156
|
+
const src = join(PUBLIC_DIR, entry);
|
|
157
|
+
if (entry === "js" || entry === "styles.css") continue;
|
|
158
|
+
if (entry.endsWith(".html")) continue;
|
|
159
|
+
if (statSync(src).isDirectory()) {
|
|
160
|
+
cpSync(src, join(outDir, entry), { recursive: true });
|
|
161
|
+
} else {
|
|
162
|
+
cpSync(src, join(outDir, entry));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 4. Render index.html with rewritten asset references.
|
|
167
|
+
const htmlSrc = join(PUBLIC_DIR, "index.html");
|
|
168
|
+
if (!existsSync(htmlSrc)) {
|
|
169
|
+
throw new Error(`Missing ${htmlSrc}`);
|
|
170
|
+
}
|
|
171
|
+
let html = readFileSync(htmlSrc, "utf8");
|
|
172
|
+
// Use relative paths so the bundle works whether the build is mounted at
|
|
173
|
+
// "/" or "/app/" (preview on Vercel).
|
|
174
|
+
html = html.replace(/"\/?js\/main\.js"/g, `"js/${jsName}"`);
|
|
175
|
+
html = html.replace(/"\/?styles\.css"/g, `"${cssName}"`);
|
|
176
|
+
writeFileSync(join(outDir, "index.html"), html);
|
|
177
|
+
|
|
178
|
+
// 5. version.json — consumed by support/debug, also lets a status check
|
|
179
|
+
// confirm which bundle a host is serving.
|
|
180
|
+
const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));
|
|
181
|
+
writeFileSync(
|
|
182
|
+
join(outDir, "version.json"),
|
|
183
|
+
JSON.stringify(
|
|
184
|
+
{
|
|
185
|
+
name: pkg.name,
|
|
186
|
+
version: pkg.version,
|
|
187
|
+
edition,
|
|
188
|
+
builtAt: new Date().toISOString()
|
|
189
|
+
},
|
|
190
|
+
null,
|
|
191
|
+
2
|
|
192
|
+
) + "\n"
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const ext = extname(jsName);
|
|
196
|
+
console.log(`✓ ${edition} build → ${outDir}`);
|
|
197
|
+
console.log(` js: js/${jsName} (${(jsFile.contents.length / 1024).toFixed(1)} KB${ext === ".js" ? " min" : ""})`);
|
|
198
|
+
console.log(` css: ${cssName} (${(cssBuf.length / 1024).toFixed(1)} KB)`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
run().catch((err) => {
|
|
202
|
+
console.error(err);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|