@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.
Files changed (86) hide show
  1. package/.dockerignore +13 -0
  2. package/Dockerfile +34 -0
  3. package/README.md +216 -0
  4. package/SKILL-ORBIT.md +94 -0
  5. package/build.mjs +204 -0
  6. package/dist/full/apple-touch-icon.png +0 -0
  7. package/dist/full/cursors/live_arrow.cur +0 -0
  8. package/dist/full/cursors/live_link.cur +0 -0
  9. package/dist/full/cursors/live_move.cur +0 -0
  10. package/dist/full/cursors/live_select.cur +0 -0
  11. package/dist/full/cursors/live_unavail.cur +0 -0
  12. package/dist/full/favicon-32x32.png +0 -0
  13. package/dist/full/index.html +314 -0
  14. package/dist/full/js/app.70be7d06.js +554 -0
  15. package/dist/full/js/app.70be7d06.js.map +7 -0
  16. package/dist/full/orbits-favicon.png +0 -0
  17. package/dist/full/orbits-logo-bg.png +0 -0
  18. package/dist/full/orbits-logo.png +0 -0
  19. package/dist/full/styles.c2e3d20a.css +4892 -0
  20. package/dist/full/version.json +6 -0
  21. package/docs/AGENT_PROTOCOL.md +379 -0
  22. package/docs/MCP_SETUP.md +49 -0
  23. package/docs/mcp-client.example.json +13 -0
  24. package/package.json +62 -0
  25. package/pnpm-workspace.yaml +2 -0
  26. package/public/apple-touch-icon.png +0 -0
  27. package/public/cursors/live_arrow.cur +0 -0
  28. package/public/cursors/live_link.cur +0 -0
  29. package/public/cursors/live_move.cur +0 -0
  30. package/public/cursors/live_select.cur +0 -0
  31. package/public/cursors/live_unavail.cur +0 -0
  32. package/public/favicon-32x32.png +0 -0
  33. package/public/index.html +314 -0
  34. package/public/js/api.js +35 -0
  35. package/public/js/app.js +35 -0
  36. package/public/js/board-menu.js +145 -0
  37. package/public/js/config.js +19 -0
  38. package/public/js/create-card.js +153 -0
  39. package/public/js/dom.js +19 -0
  40. package/public/js/drawer.js +137 -0
  41. package/public/js/format.js +108 -0
  42. package/public/js/kanban.js +475 -0
  43. package/public/js/local-backend.js +934 -0
  44. package/public/js/main.js +142 -0
  45. package/public/js/modal.js +31 -0
  46. package/public/js/router.js +114 -0
  47. package/public/js/search.js +53 -0
  48. package/public/js/settings-tabs.js +7 -0
  49. package/public/js/settings.js +931 -0
  50. package/public/js/sse.js +156 -0
  51. package/public/js/starscape.js +315 -0
  52. package/public/js/state.js +54 -0
  53. package/public/js/ticket-detail.js +766 -0
  54. package/public/js/toast.js +74 -0
  55. package/public/js/transport.js +22 -0
  56. package/public/js/unread.js +121 -0
  57. package/public/orbits-favicon.png +0 -0
  58. package/public/orbits-logo-bg.png +0 -0
  59. package/public/orbits-logo.png +0 -0
  60. package/public/styles.css +4892 -0
  61. package/src/cli/orbit.js +438 -0
  62. package/src/core/agent.js +262 -0
  63. package/src/core/auth.js +49 -0
  64. package/src/core/backups.js +136 -0
  65. package/src/core/board.js +112 -0
  66. package/src/core/boards.js +310 -0
  67. package/src/core/bootstrap.js +162 -0
  68. package/src/core/db.js +237 -0
  69. package/src/core/delete-board.js +53 -0
  70. package/src/core/events.js +314 -0
  71. package/src/core/http.js +81 -0
  72. package/src/core/paths.js +25 -0
  73. package/src/core/provision-repo-board.js +113 -0
  74. package/src/core/queries.js +246 -0
  75. package/src/core/registry.js +207 -0
  76. package/src/core/relations.js +163 -0
  77. package/src/core/router.js +414 -0
  78. package/src/core/search.js +70 -0
  79. package/src/core/seed.js +226 -0
  80. package/src/core/snapshots.js +255 -0
  81. package/src/core/states.js +167 -0
  82. package/src/core/system-picker.js +93 -0
  83. package/src/core/tickets.js +344 -0
  84. package/src/core/util.js +105 -0
  85. package/src/mcp-server.js +773 -0
  86. package/src/server.js +48 -0
package/.dockerignore ADDED
@@ -0,0 +1,13 @@
1
+ .git
2
+ .claude
3
+ .orbit
4
+ data
5
+ dist
6
+ node_modules
7
+ npm-debug.log*
8
+ pnpm-debug.log*
9
+ *.db
10
+ *.db-shm
11
+ *.db-wal
12
+ .env
13
+ .DS_Store
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