trekoon 0.4.0 → 0.4.2
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/.agents/skills/trekoon/SKILL.md +20 -577
- package/.agents/skills/trekoon/reference/execution-with-team.md +21 -9
- package/.agents/skills/trekoon/reference/execution.md +246 -7
- package/.agents/skills/trekoon/reference/planning.md +138 -1
- package/.agents/skills/trekoon/reference/status-machine.md +21 -0
- package/.agents/skills/trekoon/reference/sync.md +129 -0
- package/README.md +8 -7
- package/docs/ai-agents.md +17 -2
- package/docs/commands.md +147 -3
- package/docs/machine-contracts.md +123 -0
- package/docs/quickstart.md +52 -0
- package/package.json +1 -1
- package/src/board/assets/app.js +49 -16
- package/src/board/assets/components/Component.js +22 -8
- package/src/board/assets/components/Workspace.js +9 -3
- package/src/board/assets/components/helpers.js +5 -1
- package/src/board/assets/runtime/delegation.js +8 -0
- package/src/board/assets/runtime/focus-trap.js +48 -0
- package/src/board/assets/state/actions.js +47 -4
- package/src/board/assets/state/api.js +284 -11
- package/src/board/assets/state/store.js +87 -11
- package/src/board/assets/state/url.js +10 -0
- package/src/board/assets/state/utils.js +2 -1
- package/src/board/event-bus.ts +72 -0
- package/src/board/routes.ts +412 -33
- package/src/board/server.ts +77 -8
- package/src/board/wal-watcher.ts +302 -0
- package/src/commands/board.ts +52 -17
- package/src/commands/epic.ts +7 -9
- package/src/commands/error-utils.ts +54 -1
- package/src/commands/help.ts +69 -4
- package/src/commands/migrate.ts +153 -24
- package/src/commands/quickstart.ts +7 -0
- package/src/commands/subtask.ts +71 -10
- package/src/commands/suggest.ts +6 -13
- package/src/commands/task.ts +137 -88
- package/src/domain/batch-validation.ts +329 -0
- package/src/domain/cascade-planner.ts +412 -0
- package/src/domain/dependency-rules.ts +15 -0
- package/src/domain/mutation-service.ts +828 -192
- package/src/domain/search.ts +113 -0
- package/src/domain/tracker-domain.ts +150 -680
- package/src/domain/types.ts +53 -2
- package/src/index.ts +37 -0
- package/src/runtime/cli-shell.ts +44 -0
- package/src/runtime/daemon.ts +639 -0
- package/src/storage/backup.ts +166 -0
- package/src/storage/database.ts +261 -4
- package/src/storage/migrations.ts +422 -20
- package/src/storage/path.ts +8 -0
- package/src/storage/schema.ts +5 -1
- package/src/sync/event-writes.ts +38 -11
- package/src/sync/git-context.ts +226 -8
- package/src/sync/service.ts +650 -147
package/docs/ai-agents.md
CHANGED
|
@@ -89,6 +89,10 @@ Typical flow:
|
|
|
89
89
|
4. Execute the plan (reads `reference/execution.md` internally)
|
|
90
90
|
5. Update progress, blockers, and completion state as you go
|
|
91
91
|
|
|
92
|
+
> **Experimental — not for routine agent use:** `trekoon serve` and the
|
|
93
|
+
> `--daemon` flag are experimental. Use the default one-shot CLI path for
|
|
94
|
+
> any production or automated agent workflow until daemon mode stabilizes.
|
|
95
|
+
|
|
92
96
|
## Default execution loop
|
|
93
97
|
|
|
94
98
|
The core loop: **session, work, task done, repeat**.
|
|
@@ -113,14 +117,24 @@ If the session shows you're behind, pull tracker events before claiming work:
|
|
|
113
117
|
trekoon --toon sync pull --from main
|
|
114
118
|
```
|
|
115
119
|
|
|
116
|
-
Claim work
|
|
120
|
+
Claim work atomically using SQL compare-and-swap (recommended for parallel
|
|
121
|
+
agents), then finish or report a block:
|
|
117
122
|
|
|
118
123
|
```bash
|
|
119
|
-
trekoon --toon task
|
|
124
|
+
trekoon --toon task claim <task-id> --owner "agent-1"
|
|
120
125
|
trekoon --toon task done <task-id>
|
|
121
126
|
trekoon --toon task update <task-id> --append "Blocked by <reason>" --status blocked
|
|
122
127
|
```
|
|
123
128
|
|
|
129
|
+
`task claim` is the safe primitive for parallel agents. Two concurrent calls on
|
|
130
|
+
the same task return exactly one `claimed: true`. Check `data.claimed` before
|
|
131
|
+
proceeding — if false, another agent won the race.
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# Fallback: non-atomic claim (single-agent or sequential workflows only)
|
|
135
|
+
trekoon --toon task update <task-id> --status in_progress --owner "agent-1"
|
|
136
|
+
```
|
|
137
|
+
|
|
124
138
|
Use `task done` when the task is actually finished. It marks the task complete,
|
|
125
139
|
auto-transitions through `in_progress` if needed, reports newly unblocked
|
|
126
140
|
downstream tasks, warns about incomplete subtasks, and returns the next ready
|
|
@@ -201,6 +215,7 @@ Use the narrowest command that answers the question:
|
|
|
201
215
|
| One task with subtasks | `trekoon --toon task show <task-id> --all` |
|
|
202
216
|
| One epic tree | `trekoon --toon epic show <epic-id> --all` |
|
|
203
217
|
| Export epic to Markdown | `trekoon --toon epic export <epic-id>` |
|
|
218
|
+
| Snapshot the DB before manual recovery | `trekoon --toon migrate backup [--retain <n>]` |
|
|
204
219
|
| Repeated text in one scope | `trekoon --toon epic|task|subtask search ...` |
|
|
205
220
|
|
|
206
221
|
For repeated text changes, use the safe replace loop:
|
package/docs/commands.md
CHANGED
|
@@ -12,11 +12,11 @@ the quickest way to get started, read [Quickstart](quickstart.md) first.
|
|
|
12
12
|
- `trekoon epic <create|expand|list|show|search|replace|update|delete|progress>`
|
|
13
13
|
- `trekoon session [--epic <epic-id>]`
|
|
14
14
|
- `trekoon suggest [--epic <epic-id>]`
|
|
15
|
-
- `trekoon task <create|create-many|list|show|ready|next|done|search|replace|update|delete>`
|
|
16
|
-
- `trekoon subtask <create|create-many|list|search|replace|update|delete>`
|
|
15
|
+
- `trekoon task <create|create-many|list|show|ready|next|done|search|replace|update|delete|claim>`
|
|
16
|
+
- `trekoon subtask <create|create-many|list|search|replace|update|delete|claim>`
|
|
17
17
|
- `trekoon dep <add|add-many|remove|list|reverse>`
|
|
18
18
|
- `trekoon events prune [--dry-run] [--archive] [--retention-days <n>]`
|
|
19
|
-
- `trekoon migrate <status|rollback> [--to-version <n>]`
|
|
19
|
+
- `trekoon migrate <status|rollback|backup> [--to-version <n>] [--retain <n>]`
|
|
20
20
|
- `trekoon sync status [--from <branch>]`
|
|
21
21
|
- `trekoon sync pull --from <branch>`
|
|
22
22
|
- `trekoon sync resolve <conflict-id> --use ours|theirs [--dry-run]`
|
|
@@ -26,6 +26,7 @@ the quickest way to get started, read [Quickstart](quickstart.md) first.
|
|
|
26
26
|
- `trekoon skills install -g|--global [--editor opencode|claude|pi]`
|
|
27
27
|
- `trekoon skills update`
|
|
28
28
|
- `trekoon wipe --yes`
|
|
29
|
+
- `trekoon serve` (experimental)
|
|
29
30
|
|
|
30
31
|
## Global options
|
|
31
32
|
|
|
@@ -33,6 +34,7 @@ the quickest way to get started, read [Quickstart](quickstart.md) first.
|
|
|
33
34
|
- `--toon` — TOON-encoded output (preferred for agent loops)
|
|
34
35
|
- `--compact` — strips contract metadata from TOON/JSON envelopes
|
|
35
36
|
- `--compat <mode>` — explicit machine compatibility behavior
|
|
37
|
+
- `--daemon` — (experimental) route the call through a running `trekoon serve` daemon over a Unix socket; transparently falls back to the in-process CLI when no daemon is reachable. Equivalent to `TREKOON_DAEMON=1`.
|
|
36
38
|
- `--help` — root and command help
|
|
37
39
|
- `--version` — CLI version
|
|
38
40
|
|
|
@@ -73,6 +75,7 @@ Board API endpoints (all require token authentication):
|
|
|
73
75
|
| Method | Endpoint | Purpose |
|
|
74
76
|
| --- | --- | --- |
|
|
75
77
|
| `GET` | `/api/snapshot` | Full board state (epics, tasks, subtasks, deps, counts) |
|
|
78
|
+
| `GET` | `/api/snapshot/stream` | Server-sent events stream of `snapshot` and `snapshotDelta` events for live updates |
|
|
76
79
|
| `PATCH` | `/api/epics/{id}` | Update epic title, description, or status |
|
|
77
80
|
| `PATCH` | `/api/tasks/{id}` | Update task title, description, status, or owner |
|
|
78
81
|
| `PATCH` | `/api/subtasks/{id}` | Update subtask title, description, status, or owner |
|
|
@@ -81,6 +84,19 @@ Board API endpoints (all require token authentication):
|
|
|
81
84
|
| `POST` | `/api/dependencies` | Add dependency edge (sourceId, dependsOnId) |
|
|
82
85
|
| `DELETE` | `/api/dependencies?sourceId=...&dependsOnId=...` | Remove dependency |
|
|
83
86
|
|
|
87
|
+
**Note:** the SSE auth token rides as a `?token=` query parameter on
|
|
88
|
+
`/api/snapshot/stream` (EventSource cannot set custom headers), so it may
|
|
89
|
+
appear in reverse-proxy access logs; treat access logs as sensitive if the
|
|
90
|
+
board is exposed beyond localhost.
|
|
91
|
+
|
|
92
|
+
Board mutations from any source — board UI, CLI in another shell, or another
|
|
93
|
+
worktree — propagate to every connected client through the SSE stream. The CLI
|
|
94
|
+
side is driven by a WAL-watcher that diffs the snapshot when
|
|
95
|
+
`.trekoon/trekoon.db-wal` changes; in-process mutations publish deltas
|
|
96
|
+
directly. PATCH endpoints accept `If-Match: <updatedAt-ms>` for optimistic
|
|
97
|
+
concurrency: a stale value returns `409` with `currentUpdatedAt`. Missing
|
|
98
|
+
`If-Match` is allowed for back-compat.
|
|
99
|
+
|
|
84
100
|
Board commands don't accept command-specific options yet. For tests and local
|
|
85
101
|
development, `TREKOON_BOARD_ASSET_ROOT` overrides the bundled asset source.
|
|
86
102
|
|
|
@@ -183,6 +199,41 @@ trekoon subtask update <subtask-id> --owner "agent-2"
|
|
|
183
199
|
|
|
184
200
|
Also accepted on `PATCH /api/tasks/{id}` and `PATCH /api/subtasks/{id}`.
|
|
185
201
|
|
|
202
|
+
## Task claim
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
trekoon task claim <task-id> --owner <owner>
|
|
206
|
+
trekoon subtask claim <subtask-id> --owner <owner>
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Atomically claim a task (or subtask) using a SQL compare-and-swap. Sets
|
|
210
|
+
`status=in_progress` and `owner=<owner>` only when:
|
|
211
|
+
|
|
212
|
+
- The task is in `todo` or `blocked` status, **and**
|
|
213
|
+
- The `owner` column is `NULL` or already equal to `<owner>` (re-entrant claim)
|
|
214
|
+
|
|
215
|
+
Two concurrent `task claim` calls racing on the same task return exactly one
|
|
216
|
+
`claimed: true`. The loser gets `claimed: false` with `currentOwner` and
|
|
217
|
+
`currentStatus` reflecting the winner's write.
|
|
218
|
+
|
|
219
|
+
`--owner` is required.
|
|
220
|
+
|
|
221
|
+
Response envelope:
|
|
222
|
+
|
|
223
|
+
```text
|
|
224
|
+
ok: true
|
|
225
|
+
command: task.claim | subtask.claim
|
|
226
|
+
data:
|
|
227
|
+
claimed: true | false
|
|
228
|
+
currentOwner: <owner> | null
|
|
229
|
+
currentStatus: in_progress | todo | blocked | done
|
|
230
|
+
task | subtask: { ...full record... } # present only when claimed=true
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
When `claimed` is false, `task`/`subtask` is absent. `currentOwner` tells you
|
|
234
|
+
who holds the lock. `currentStatus` tells you why the claim failed (already
|
|
235
|
+
`done`, or `in_progress` by another owner).
|
|
236
|
+
|
|
186
237
|
## Task done
|
|
187
238
|
|
|
188
239
|
`trekoon task done <task-id>` marks a task complete and returns the next ready
|
|
@@ -289,6 +340,99 @@ trekoon --toon sync conflicts show <conflict-id>
|
|
|
289
340
|
`list` defaults to `--mode pending`. Use `--mode all` to include resolved
|
|
290
341
|
conflicts.
|
|
291
342
|
|
|
343
|
+
## Migrate commands
|
|
344
|
+
|
|
345
|
+
### `migrate status`
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
trekoon --toon migrate status
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Reports `currentVersion`, `latestVersion`, and pending migration count.
|
|
352
|
+
|
|
353
|
+
### `migrate rollback`
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
trekoon --toon migrate rollback [--to-version <n>]
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Rolls back schema migrations one version (default) or down to `--to-version`.
|
|
360
|
+
Migrations 0004, 0005, and 0006 are irreversible (ALTER TABLE plus dependency
|
|
361
|
+
data cleanup). Rolling back below those versions errors with code
|
|
362
|
+
`migration_down_unsupported`. Take a backup first; restore by copying the
|
|
363
|
+
backup over `.trekoon/trekoon.db`.
|
|
364
|
+
|
|
365
|
+
### `migrate backup`
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
trekoon --toon migrate backup
|
|
369
|
+
trekoon --toon migrate backup --retain 5
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Snapshots `.trekoon/trekoon.db` to a timestamped sibling file
|
|
373
|
+
(`trekoon.db.backup-<ISO8601>`) using SQLite `VACUUM INTO`. The source database
|
|
374
|
+
is opened read-only, so a backup never mutates live state. Returns
|
|
375
|
+
`backupPath`, `bytes`, `migrationVersion`, and `latestVersion` for machine
|
|
376
|
+
consumers. Backups stay inside `.trekoon/` and are gitignored along with the
|
|
377
|
+
rest of the directory.
|
|
378
|
+
|
|
379
|
+
`--retain <n>` keeps the last `n` timestamped backups; older siblings are
|
|
380
|
+
pruned in the same call. Default is `10`. Backups created within the same
|
|
381
|
+
millisecond produce a deterministic collision error that names the existing
|
|
382
|
+
file so operators can identify it.
|
|
383
|
+
|
|
384
|
+
To restore from a backup: stop any process holding the DB, then copy the
|
|
385
|
+
backup file over `.trekoon/trekoon.db`.
|
|
386
|
+
|
|
387
|
+
## Daemon (experimental)
|
|
388
|
+
|
|
389
|
+
`trekoon serve` runs Trekoon as a long-lived process listening
|
|
390
|
+
on a Unix-domain socket inside `<storage-root>/.trekoon/daemon.sock`. The
|
|
391
|
+
daemon holds the SQLite connection in memory, so subsequent invocations skip
|
|
392
|
+
Bun startup, module load, migration probes, and database open.
|
|
393
|
+
|
|
394
|
+
Status: **experimental**. Not on by default. The default one-shot CLI behavior
|
|
395
|
+
is unchanged whether or not the daemon is running.
|
|
396
|
+
|
|
397
|
+
Activate the client with one of:
|
|
398
|
+
|
|
399
|
+
```bash
|
|
400
|
+
TREKOON_DAEMON=1 trekoon session
|
|
401
|
+
trekoon --daemon session
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
If no daemon is reachable, the client transparently falls back to the in-process
|
|
405
|
+
one-shot path. Calls run against the daemon return envelopes equivalent to the
|
|
406
|
+
in-process call modulo:
|
|
407
|
+
|
|
408
|
+
- The per-request `requestId` and `persistedAt` fields.
|
|
409
|
+
- Environment variables. The client's `process.env` is **not** forwarded over
|
|
410
|
+
the socket; the daemon process uses the environment captured at `trekoon
|
|
411
|
+
serve` startup. If a command's behavior depends on an env var (e.g. an
|
|
412
|
+
override flag), set it before launching the daemon — not on the client side.
|
|
413
|
+
|
|
414
|
+
The daemon request contract is `{argv, cwd}` only — there is no per-call env
|
|
415
|
+
channel. This narrows the secret-leak surface area and keeps the equivalence
|
|
416
|
+
claim crisp.
|
|
417
|
+
|
|
418
|
+
Security:
|
|
419
|
+
|
|
420
|
+
- Socket file mode `0o600`; parent `.trekoon/` directory forced to `0o700`.
|
|
421
|
+
- The umask is tightened to `0o077` around `server.listen()` so the socket
|
|
422
|
+
inode is created with restrictive perms (no TOCTOU window before chmod).
|
|
423
|
+
- Daemon error envelopes return a sanitized message only — no stack trace,
|
|
424
|
+
which would otherwise leak filesystem paths or secret-bearing error text.
|
|
425
|
+
Stacks are still routed to the daemon's local `console.error` for operator
|
|
426
|
+
debugging.
|
|
427
|
+
- Client env is not transmitted over the socket (see equivalence note above).
|
|
428
|
+
- Stale sockets from prior crashes are unlinked at startup.
|
|
429
|
+
- On `Ctrl-C` / `SIGTERM` the socket is unlinked and the cached database
|
|
430
|
+
connection is closed.
|
|
431
|
+
|
|
432
|
+
Performance (see `bench/daemon-session.ts`): daemon median < 10 ms,
|
|
433
|
+
cold one-shot median > 50 ms — roughly a 5–10× latency reduction for
|
|
434
|
+
repeated calls by reusing the held-open SQLite connection.
|
|
435
|
+
|
|
292
436
|
## Related docs
|
|
293
437
|
|
|
294
438
|
- [Quickstart](quickstart.md)
|
|
@@ -254,6 +254,12 @@ data:
|
|
|
254
254
|
Transition details are in `data`, not `error.details`. `error` only has `code`
|
|
255
255
|
and `message`.
|
|
256
256
|
|
|
257
|
+
**Allowed bypass:** `task done` performs an atomic single-transaction direct
|
|
258
|
+
write to `done` from any non-`done` status (`todo`, `blocked`, `in_progress`),
|
|
259
|
+
emitting one `task.updated` event — there is no intermediate `in_progress`
|
|
260
|
+
event when starting from `todo`/`blocked`. This is the only sanctioned
|
|
261
|
+
bypass of the status-machine checker.
|
|
262
|
+
|
|
257
263
|
## Epic progress
|
|
258
264
|
|
|
259
265
|
```bash
|
|
@@ -328,6 +334,69 @@ data:
|
|
|
328
334
|
pendingConflicts
|
|
329
335
|
```
|
|
330
336
|
|
|
337
|
+
## Task claim
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
trekoon --toon task claim <task-id> --owner <owner>
|
|
341
|
+
trekoon --toon subtask claim <subtask-id> --owner <owner>
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Atomically claims a task or subtask using SQL compare-and-swap. The single
|
|
345
|
+
UPDATE predicate ensures exactly one concurrent caller gets `claimed: true`.
|
|
346
|
+
|
|
347
|
+
Success (claimed):
|
|
348
|
+
|
|
349
|
+
```text
|
|
350
|
+
ok: true
|
|
351
|
+
command: task.claim | subtask.claim
|
|
352
|
+
data:
|
|
353
|
+
claimed: true
|
|
354
|
+
currentOwner: <owner>
|
|
355
|
+
currentStatus: in_progress
|
|
356
|
+
task | subtask: { ...full record... }
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Not claimed (another owner holds it, or status is done/in_progress by others):
|
|
360
|
+
|
|
361
|
+
```text
|
|
362
|
+
ok: true
|
|
363
|
+
command: task.claim | subtask.claim
|
|
364
|
+
data:
|
|
365
|
+
claimed: false
|
|
366
|
+
currentOwner: <string> | null
|
|
367
|
+
currentStatus: in_progress | done | todo | blocked
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Note: `claimed: false` is a successful response (`ok: true`). The command
|
|
371
|
+
reports the current state, not an error condition. Check `data.claimed` to
|
|
372
|
+
distinguish the two cases.
|
|
373
|
+
|
|
374
|
+
Dependency gating on claim (cr-expert hardening): when the task or subtask is
|
|
375
|
+
in `blocked` or `todo` and has unresolved dependencies, the claim atomically
|
|
376
|
+
fails with `code: dependency_blocked` rather than flipping the row into
|
|
377
|
+
`in_progress`. This mirrors `task done` semantics so neither
|
|
378
|
+
forward-progress transition (`todo|blocked → in_progress` via claim,
|
|
379
|
+
`* → done` via `task done`) can bypass dependency resolution.
|
|
380
|
+
|
|
381
|
+
```text
|
|
382
|
+
ok: false
|
|
383
|
+
command: task.claim | subtask.claim
|
|
384
|
+
error:
|
|
385
|
+
code: dependency_blocked
|
|
386
|
+
message: "task cannot transition to in_progress while dependencies are unresolved"
|
|
387
|
+
data:
|
|
388
|
+
unresolvedDependencyCount: <number>
|
|
389
|
+
unresolvedDependencyIds: [<id>, ...]
|
|
390
|
+
unresolvedDependencies: [{ id, kind, status }, ...]
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
The only intentional direct-status-write exception in the codebase remains
|
|
394
|
+
`MutationService.markTaskDoneAtomically`, which bypasses
|
|
395
|
+
`validateStatusTransition` for the `* → done` flip but still goes through
|
|
396
|
+
`assertNoUnresolvedDependenciesForStatusTransition` before issuing the UPDATE.
|
|
397
|
+
No other call site is permitted to write `status` directly without going
|
|
398
|
+
through the public transition checker.
|
|
399
|
+
|
|
331
400
|
## Owner field in updates
|
|
332
401
|
|
|
333
402
|
Task and subtask update payloads include `owner`:
|
|
@@ -538,6 +607,60 @@ error:
|
|
|
538
607
|
message: "Pending conflicts changed before batch resolution could be applied."
|
|
539
608
|
```
|
|
540
609
|
|
|
610
|
+
## Error code registry
|
|
611
|
+
|
|
612
|
+
All machine-visible error codes emitted by Trekoon. Every `error.code` value in
|
|
613
|
+
any `ok: false` response will be one of the following strings.
|
|
614
|
+
|
|
615
|
+
| Code | Description |
|
|
616
|
+
|---|---|
|
|
617
|
+
| `already_done` | Entity is already in `done` status; transition is a no-op. |
|
|
618
|
+
| `already_resolved` | Conflict was resolved by another process before this write. |
|
|
619
|
+
| `ambiguous_legacy_state` | Legacy worktree state is ambiguous and cannot be automatically resolved. |
|
|
620
|
+
| `backpressure` | SSE snapshot stream disconnected a slow client whose queued bytes exceeded the hard limit. |
|
|
621
|
+
| `backup_already_exists` | A backup file already exists at the target path; overwrite was not requested. |
|
|
622
|
+
| `backup_database_missing` | Source database file not found when attempting to create a backup. |
|
|
623
|
+
| `backup_failed` | Backup operation failed (I/O or copy error). |
|
|
624
|
+
| `cancelled` | Operation was cancelled by the user (e.g. confirmation prompt rejected). |
|
|
625
|
+
| `confirmation_required` | Human-mode operation requires explicit confirmation before proceeding. |
|
|
626
|
+
| `conflict_set_changed` | Batch conflict set changed between preview and confirmed write. |
|
|
627
|
+
| `daemon_start_failed` | Daemon failed to start (socket bind error or already running). |
|
|
628
|
+
| `database_busy` | SQLite database is locked; retry after a short wait. |
|
|
629
|
+
| `dependency_blocked` | Cascade update blocked because one or more descendants have unresolved dependencies. |
|
|
630
|
+
| `disallowed_field` | Sync resolve attempted to write a field that is not on the allow-list. |
|
|
631
|
+
| `events_failed` | Event log query or export failed. |
|
|
632
|
+
| `install_failed` | Skill installation failed (file copy or permission error). |
|
|
633
|
+
| `internal_error` | Unexpected internal error; inspect server logs for details. |
|
|
634
|
+
| `invalid_args` | Required positional arguments are missing or malformed. |
|
|
635
|
+
| `invalid_dependency` | Dependency edge is invalid (self-loop, wrong entity type, or duplicate). |
|
|
636
|
+
| `invalid_input` | One or more option values failed validation. |
|
|
637
|
+
| `invalid_path` | File path is invalid for the requested operation (e.g. path is a directory). |
|
|
638
|
+
| `invalid_source` | Source entity for a dependency or sync operation does not exist or is of wrong kind. |
|
|
639
|
+
| `invalid_state` | Entity or system is in a state that does not permit the requested operation. |
|
|
640
|
+
| `invalid_subcommand` | Unrecognised subcommand for this command group. |
|
|
641
|
+
| `legacy_import_failed` | Import from a legacy (pre-`.trekoon`) data directory failed. |
|
|
642
|
+
| `migrate_failed` | Database schema migration failed. |
|
|
643
|
+
| `migration_down_unsupported` | Down-migrations are not supported; only forward migrations are allowed. |
|
|
644
|
+
| `missing_asset` | Expected bundled asset (skill file, template) was not found. |
|
|
645
|
+
| `no_matching_conflicts` | `sync resolve --all` filters matched zero pending conflicts. |
|
|
646
|
+
| `not_found` | Requested entity (epic, task, subtask, dependency) does not exist. |
|
|
647
|
+
| `orphaned_external_node` | Dependency graph contains a node that belongs to a different epic. |
|
|
648
|
+
| `outside_repo_target` | Skill install target path is outside the repository root. |
|
|
649
|
+
| `permission_denied` | File-system permission denied for the requested path. |
|
|
650
|
+
| `precondition_failed` | `If-Match` precondition header did not match the entity's current `updatedAt`. |
|
|
651
|
+
| `row_not_found` | Sync resolve target row no longer exists in the database. |
|
|
652
|
+
| `status_transition_invalid` | Requested status transition is not permitted by the status machine. |
|
|
653
|
+
| `stream_unavailable` | SSE snapshot stream is not available (board not initialised or shutting down). |
|
|
654
|
+
| `sync_failed` | Sync pull or push operation failed. |
|
|
655
|
+
| `tracked_ignored_mismatch` | Worktree is tracked by Trekoon but its path is in `.gitignore`, or vice-versa. |
|
|
656
|
+
| `unauthorized` | Request lacks valid authentication credentials. |
|
|
657
|
+
| `unhandled_command` | Command matched a known group but no case handled the subcommand. |
|
|
658
|
+
| `unknown_command` | Top-level command token is not recognised. |
|
|
659
|
+
| `unknown_option` | An option flag passed to the command is not recognised. |
|
|
660
|
+
| `unsupported_entity_kind` | Sync resolve encountered an entity kind that the writer does not handle. |
|
|
661
|
+
| `update_failed` | Skill update failed (download, copy, or permission error). |
|
|
662
|
+
| `wrong_entity_type` | Operation requires a specific entity kind but a different kind was supplied. |
|
|
663
|
+
|
|
541
664
|
## Related docs
|
|
542
665
|
|
|
543
666
|
- [Quickstart](quickstart.md)
|
package/docs/quickstart.md
CHANGED
|
@@ -148,6 +148,58 @@ a file extension means "write this file"; no extension means "put the default-
|
|
|
148
148
|
named file in this directory". Use `--overwrite` to resave after the plan state
|
|
149
149
|
changes.
|
|
150
150
|
|
|
151
|
+
## Claim work atomically
|
|
152
|
+
|
|
153
|
+
When multiple agents or lanes work in parallel, use `claim` to avoid races:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
trekoon task claim <task-id> --owner <owner>
|
|
157
|
+
trekoon subtask claim <subtask-id> --owner <owner>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Both commands use a SQL compare-and-swap: the claim succeeds only when the item
|
|
161
|
+
is `todo` or `blocked` and the owner is `NULL` or already set to `<owner>`.
|
|
162
|
+
The response includes `claimed` (true/false), `currentOwner`, `currentStatus`,
|
|
163
|
+
and the full entity record on success. Two concurrent claims return exactly one
|
|
164
|
+
`claimed=true`.
|
|
165
|
+
|
|
166
|
+
## Database backup and migration
|
|
167
|
+
|
|
168
|
+
Before any manual migration recovery, snapshot the database:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
trekoon migrate backup
|
|
172
|
+
trekoon migrate backup --retain 5 # keep last 5 backups (default 10)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
This writes a timestamped copy of `.trekoon/trekoon.db` next to the live file.
|
|
176
|
+
To check the current schema version or roll back:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
trekoon migrate status
|
|
180
|
+
trekoon migrate rollback # one version back
|
|
181
|
+
trekoon migrate rollback --to-version 1
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Daemon mode (experimental)
|
|
185
|
+
|
|
186
|
+
> **Not for routine agent use.** `trekoon serve` and `--daemon` are
|
|
187
|
+
> experimental; use the default one-shot CLI path in automated workflows.
|
|
188
|
+
|
|
189
|
+
`trekoon serve` starts a foreground daemon on a Unix-domain socket inside
|
|
190
|
+
`.trekoon`. Subsequent calls skip Bun startup, module load, and database
|
|
191
|
+
open — per `bench/daemon-session.ts`: daemon median < 10 ms vs. cold
|
|
192
|
+
one-shot median > 50 ms:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
trekoon serve # start the daemon (foreground)
|
|
196
|
+
trekoon --daemon session # route a call through the daemon
|
|
197
|
+
TREKOON_DAEMON=1 trekoon session # same, via environment variable
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
If the socket is missing or unreachable the client falls back silently to the
|
|
201
|
+
normal one-shot path. The socket is 0o600 and `.trekoon` is forced to 0o700.
|
|
202
|
+
|
|
151
203
|
## Check progress
|
|
152
204
|
|
|
153
205
|
```bash
|
package/package.json
CHANGED
package/src/board/assets/app.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createBoardActions } from "./state/actions.js";
|
|
2
|
-
import { createApi } from "./state/api.js";
|
|
2
|
+
import { createApi, subscribeSnapshotStream } from "./state/api.js";
|
|
3
3
|
import { applyTheme, createStore, readThemePreference } from "./state/store.js";
|
|
4
4
|
import { normalizeSnapshot, normalizeStatus } from "./state/utils.js";
|
|
5
5
|
import { syncUrlHash } from "./state/url.js";
|
|
6
6
|
import { createDelegation } from "./runtime/delegation.js";
|
|
7
|
+
import { createOverlayFocusTrap } from "./runtime/focus-trap.js";
|
|
7
8
|
import { createTopBar } from "./components/TopBar.js";
|
|
8
9
|
import { createWorkspace } from "./components/Workspace.js";
|
|
9
10
|
import { createTaskModal } from "./components/TaskModal.js";
|
|
@@ -132,15 +133,16 @@ export async function bootLegacyBoard(options = {}) {
|
|
|
132
133
|
const runtimeSession = resolveRuntimeSession();
|
|
133
134
|
if (runtimeSession.shouldScrubAddressBar) scrubTokenFromAddressBar();
|
|
134
135
|
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (!payload?.ok) throw new Error(payload?.error?.message || "Board request failed");
|
|
142
|
-
snapshotPayload = payload?.data?.snapshot ?? {};
|
|
136
|
+
// Always fetch snapshot client-side. The bootstrap script in index.html
|
|
137
|
+
// only carries the auth token; the snapshot is fetched via /api/snapshot
|
|
138
|
+
// so index.html stays small and never carries board data in its HTML body.
|
|
139
|
+
const response = await fetch("/api/snapshot");
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
throw new Error(`Board snapshot request failed with status ${response.status}`);
|
|
143
142
|
}
|
|
143
|
+
const payload = await response.json();
|
|
144
|
+
if (!payload?.ok) throw new Error(payload?.error?.message || "Board request failed");
|
|
145
|
+
const snapshotPayload = payload?.data?.snapshot ?? {};
|
|
144
146
|
|
|
145
147
|
const snapshot = normalizeSnapshot(snapshotPayload);
|
|
146
148
|
|
|
@@ -258,6 +260,12 @@ export async function bootLegacyBoard(options = {}) {
|
|
|
258
260
|
overlay.focus({ preventScroll: true });
|
|
259
261
|
}
|
|
260
262
|
|
|
263
|
+
const overlayFocusTrap = createOverlayFocusTrap({
|
|
264
|
+
doc: document,
|
|
265
|
+
onTabKey: (event) => trapOverlayFocus(event),
|
|
266
|
+
onFocusIn: (event) => containOverlayFocus(event),
|
|
267
|
+
});
|
|
268
|
+
|
|
261
269
|
function syncOverlayEnvironment() {
|
|
262
270
|
const hadOverlay = activeOverlay instanceof HTMLElement;
|
|
263
271
|
const nextOverlay = getActiveOverlayElement();
|
|
@@ -271,6 +279,10 @@ export async function bootLegacyBoard(options = {}) {
|
|
|
271
279
|
lockBackgroundScroll();
|
|
272
280
|
setBackgroundInert(true);
|
|
273
281
|
}
|
|
282
|
+
// Attach the document-level focus trap only while an overlay is actually
|
|
283
|
+
// open. This avoids the microtask window where Tab keydowns get swallowed
|
|
284
|
+
// by a stale handler with no overlay to confine to.
|
|
285
|
+
overlayFocusTrap.attach();
|
|
274
286
|
queueMicrotask(() => focusOverlay(nextOverlay));
|
|
275
287
|
return;
|
|
276
288
|
}
|
|
@@ -280,6 +292,7 @@ export async function bootLegacyBoard(options = {}) {
|
|
|
280
292
|
unlockBackgroundScroll();
|
|
281
293
|
setBackgroundInert(false);
|
|
282
294
|
}
|
|
295
|
+
overlayFocusTrap.detach();
|
|
283
296
|
queueMicrotask(() => restoreOverlayFocus());
|
|
284
297
|
}
|
|
285
298
|
|
|
@@ -355,8 +368,9 @@ export async function bootLegacyBoard(options = {}) {
|
|
|
355
368
|
return true;
|
|
356
369
|
}
|
|
357
370
|
|
|
358
|
-
|
|
359
|
-
|
|
371
|
+
// Focus-trap listeners are attached lazily by syncOverlayEnvironment when an
|
|
372
|
+
// overlay actually opens (and detached when it closes), so plain Tab outside
|
|
373
|
+
// any overlay reaches normal browser flow.
|
|
360
374
|
|
|
361
375
|
// Render cycle
|
|
362
376
|
function rerender() {
|
|
@@ -365,7 +379,9 @@ export async function bootLegacyBoard(options = {}) {
|
|
|
365
379
|
const screen = boardState.screen;
|
|
366
380
|
const selectedTask = boardState.selectedTask;
|
|
367
381
|
const selectedSubtask = boardState.selectedSubtask;
|
|
368
|
-
const
|
|
382
|
+
const taskModalOpen = Boolean(boardState.taskModalOpen && selectedTask);
|
|
383
|
+
const subtaskModalOpen = Boolean(boardState.subtaskModalOpen && selectedSubtask);
|
|
384
|
+
const currentNav = taskModalOpen ? "detail" : screen === "tasks" ? "board" : "epics";
|
|
369
385
|
|
|
370
386
|
// Layout toggles
|
|
371
387
|
const showTasks = screen === "tasks" && boardState.selectedEpic;
|
|
@@ -381,11 +397,11 @@ export async function bootLegacyBoard(options = {}) {
|
|
|
381
397
|
confirmDialog.update(null);
|
|
382
398
|
}
|
|
383
399
|
|
|
384
|
-
if (!showTasks || !
|
|
400
|
+
if (!showTasks || !taskModalOpen) {
|
|
385
401
|
taskModal.update(null);
|
|
386
402
|
}
|
|
387
403
|
|
|
388
|
-
if (!showTasks || !
|
|
404
|
+
if (!showTasks || !taskModalOpen || !subtaskModalOpen) {
|
|
389
405
|
subtaskModal.update(null);
|
|
390
406
|
}
|
|
391
407
|
|
|
@@ -428,7 +444,7 @@ export async function bootLegacyBoard(options = {}) {
|
|
|
428
444
|
visibleTasks: boardState.visibleTasks,
|
|
429
445
|
});
|
|
430
446
|
|
|
431
|
-
taskModal.update(
|
|
447
|
+
taskModal.update(taskModalOpen ? {
|
|
432
448
|
task: selectedTask,
|
|
433
449
|
epics: store.snapshot.epics,
|
|
434
450
|
snapshot: store.snapshot,
|
|
@@ -442,7 +458,7 @@ export async function bootLegacyBoard(options = {}) {
|
|
|
442
458
|
});
|
|
443
459
|
}
|
|
444
460
|
|
|
445
|
-
subtaskModal.update(showTasks &&
|
|
461
|
+
subtaskModal.update(showTasks && taskModalOpen && subtaskModalOpen ? {
|
|
446
462
|
subtask: selectedSubtask,
|
|
447
463
|
isMutating: store.isMutating,
|
|
448
464
|
} : null);
|
|
@@ -452,6 +468,22 @@ export async function bootLegacyBoard(options = {}) {
|
|
|
452
468
|
|
|
453
469
|
const api = createApi(model, { sessionToken: runtimeSession.token, rerender });
|
|
454
470
|
|
|
471
|
+
// Subscribe to /api/snapshot/stream so external CLI writes (picked up by
|
|
472
|
+
// the WAL watcher) and concurrent-tab mutations are reflected in this
|
|
473
|
+
// board within ~1s without manual refresh. applySnapshotDelta is idempotent
|
|
474
|
+
// so re-receiving deltas already applied via mutation responses is safe.
|
|
475
|
+
// Capture the handle so we can tear it down on page unload (and so tests
|
|
476
|
+
// and future teardown paths can dispose the EventSource).
|
|
477
|
+
const snapshotSubscription = subscribeSnapshotStream(model, {
|
|
478
|
+
sessionToken: runtimeSession.token,
|
|
479
|
+
rerender,
|
|
480
|
+
});
|
|
481
|
+
if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
|
|
482
|
+
window.addEventListener("beforeunload", () => {
|
|
483
|
+
snapshotSubscription.dispose();
|
|
484
|
+
}, { once: true });
|
|
485
|
+
}
|
|
486
|
+
|
|
455
487
|
// Actions for delegation
|
|
456
488
|
const actions = createBoardActions({
|
|
457
489
|
model,
|
|
@@ -550,6 +582,7 @@ export async function bootLegacyBoard(options = {}) {
|
|
|
550
582
|
addDependency: (src, data) => actions.addDependency(src, data),
|
|
551
583
|
dropTaskStatus: (id, status) => actions.dropTaskStatus(id, status),
|
|
552
584
|
getTaskStatus: (id) => actions.getTaskStatus(id),
|
|
585
|
+
setDragFeedback: (feedback) => actions.setDragFeedback(feedback),
|
|
553
586
|
changeEpicStatus: (epicId, status) => actions.changeEpicStatus(epicId, status),
|
|
554
587
|
bulkSetStatus: (epicId, status) => actions.bulkSetStatus(epicId, status),
|
|
555
588
|
toggleEpicStatusFilter: (status) => actions.toggleEpicStatusFilter(status),
|