trekoon 0.4.2 → 0.4.3
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 +97 -208
- package/.agents/skills/trekoon/reference/execution-with-team.md +87 -149
- package/.agents/skills/trekoon/reference/execution.md +170 -380
- package/.agents/skills/trekoon/reference/harness-primitives.md +77 -0
- package/.agents/skills/trekoon/reference/planning.md +193 -330
- package/.agents/skills/trekoon/reference/sync.md +56 -103
- package/README.md +21 -7
- package/docs/ai-agents.md +48 -4
- package/docs/commands.md +2 -2
- package/docs/quickstart.md +3 -3
- package/package.json +1 -1
- package/src/board/assets/app.js +4 -2
- package/src/board/assets/components/Component.js +6 -8
- package/src/board/assets/state/actions.js +3 -0
- package/src/board/assets/state/api.js +48 -34
- package/src/board/assets/state/store.js +3 -0
- package/src/board/event-bus.ts +9 -0
- package/src/board/routes.ts +94 -83
- package/src/board/server.ts +14 -7
- package/src/board/snapshot.ts +6 -0
- package/src/board/wal-watcher.ts +22 -11
- package/src/commands/help.ts +6 -6
- package/src/commands/skills.ts +17 -5
- package/src/domain/mutation-service.ts +61 -42
- package/src/domain/tracker-domain.ts +20 -16
- package/src/domain/types.ts +3 -0
- package/src/export/render-markdown.ts +1 -2
- package/src/runtime/daemon.ts +110 -49
- package/src/storage/database.ts +9 -2
- package/src/storage/migrations.ts +19 -2
- package/src/sync/service.ts +47 -27
|
@@ -1,129 +1,82 @@
|
|
|
1
1
|
# Sync Reference
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Trekoon uses one live SQLite database per repository at
|
|
4
|
+
`<sharedStorageRoot>/.trekoon/trekoon.db`. Linked worktrees share it. `git
|
|
5
|
+
checkout` and `git switch` do not roll back tracker state. Sync imports tracker
|
|
6
|
+
events between branches; never copy or commit the `.db` file.
|
|
4
7
|
|
|
5
|
-
Same-branch sync is a no-op
|
|
6
|
-
|
|
7
|
-
on the source branch. No action is needed.
|
|
8
|
+
Same-branch sync is a no-op. Cross-branch sync matters before merging a feature
|
|
9
|
+
branch back.
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
## Before Merge
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
Pull tracker events from the base branch:
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
- If conflicts exist, inspect and resolve them explicitly:
|
|
15
|
+
```bash
|
|
16
|
+
trekoon --toon sync pull --from main
|
|
17
|
+
```
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
trekoon --toon sync conflicts list
|
|
21
|
-
trekoon --toon sync conflicts show <conflict-id>
|
|
22
|
-
trekoon --toon sync resolve <conflict-id> --use theirs --dry-run
|
|
23
|
-
trekoon --toon sync resolve <conflict-id> --use ours
|
|
24
|
-
```
|
|
19
|
+
If conflicts exist:
|
|
25
20
|
|
|
26
|
-
|
|
21
|
+
```bash
|
|
22
|
+
trekoon --toon sync conflicts list
|
|
23
|
+
trekoon --toon sync conflicts show <conflict-id>
|
|
24
|
+
trekoon --toon sync resolve <conflict-id> --use theirs --dry-run
|
|
25
|
+
trekoon --toon sync resolve <conflict-id> --use ours
|
|
26
|
+
```
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
(e.g., `status`, `title`, `description`) on one entity.
|
|
28
|
+
## Conflict Rules
|
|
30
29
|
|
|
31
|
-
-
|
|
32
|
-
|
|
33
|
-
resolution event is appended.
|
|
34
|
-
- `--use theirs` — overwrite the shared DB entity field with the source-branch
|
|
35
|
-
value. The conflict record is marked resolved and a resolution event is
|
|
36
|
-
appended.
|
|
37
|
-
- `--dry-run` — preview the resolution without mutating the database. Returns
|
|
38
|
-
`oursValue`, `theirsValue`, `wouldWrite`, and `dryRun: true`. Use this before
|
|
39
|
-
committing to a resolution.
|
|
30
|
+
Conflicts are field-level, not whole-record. Each conflict targets one field on
|
|
31
|
+
one entity.
|
|
40
32
|
|
|
41
|
-
|
|
42
|
-
field
|
|
43
|
-
|
|
44
|
-
-
|
|
45
|
-
|
|
46
|
-
- `--use theirs` changes status to `done` in the live shared DB
|
|
33
|
+
- `--use ours`: keep the current shared DB field value; mark conflict resolved.
|
|
34
|
+
- `--use theirs`: write the source-branch field value into the shared DB; mark
|
|
35
|
+
conflict resolved.
|
|
36
|
+
- `--dry-run`: preview without mutation. Returns `oursValue`, `theirsValue`,
|
|
37
|
+
`wouldWrite`, and `dryRun: true`.
|
|
47
38
|
|
|
48
|
-
Always inspect
|
|
49
|
-
|
|
39
|
+
Always inspect with `sync conflicts show` before resolving. Choosing `theirs`
|
|
40
|
+
without inspection can overwrite in-progress shared DB work.
|
|
50
41
|
|
|
51
|
-
|
|
42
|
+
Typical choices:
|
|
52
43
|
|
|
53
|
-
| Scenario |
|
|
44
|
+
| Scenario | Usually use | Why |
|
|
54
45
|
|---|---|---|
|
|
55
|
-
| Completed work vs stale main
|
|
46
|
+
| Completed work vs stale main | ours | Your branch has latest progress |
|
|
56
47
|
| Enriched descriptions vs original | ours | Your descriptions are more detailed |
|
|
57
|
-
| Upstream updates from another agent | theirs | Accept
|
|
58
|
-
| User-intentional reset | theirs | Respect
|
|
48
|
+
| Upstream updates from another agent | theirs | Accept newer upstream state |
|
|
49
|
+
| User-intentional reset | theirs | Respect explicit user action |
|
|
59
50
|
|
|
60
|
-
|
|
51
|
+
When unsure, ask the user.
|
|
61
52
|
|
|
62
|
-
|
|
63
|
-
2. Group by pattern — are conflicts on the same field or direction?
|
|
64
|
-
3. If uniform pattern, batch resolve: `trekoon --toon sync resolve --all --use ours`
|
|
65
|
-
4. If mixed, narrow by entity or field, or inspect individually
|
|
66
|
-
5. When unsure, ask the user
|
|
67
|
-
|
|
68
|
-
### Batch resolve patterns
|
|
69
|
-
|
|
70
|
-
Common scenarios:
|
|
53
|
+
## Batch Resolve
|
|
71
54
|
|
|
72
55
|
```bash
|
|
73
|
-
# Resolve all conflicts at once (most common after completing work)
|
|
74
|
-
trekoon --toon sync resolve --all --use ours
|
|
75
|
-
|
|
76
|
-
# Preview before resolving
|
|
77
56
|
trekoon --toon sync resolve --all --use ours --dry-run
|
|
78
|
-
|
|
79
|
-
# Narrow to status field conflicts only
|
|
57
|
+
trekoon --toon sync resolve --all --use ours
|
|
80
58
|
trekoon --toon sync resolve --all --use ours --field status
|
|
81
|
-
|
|
82
|
-
# Narrow to a specific entity
|
|
83
59
|
trekoon --toon sync resolve --all --use theirs --entity <id>
|
|
84
|
-
|
|
85
|
-
# Combine filters
|
|
86
60
|
trekoon --toon sync resolve --all --use ours --entity <id> --field description
|
|
87
61
|
```
|
|
88
62
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
**Conflicts are scoped per worktree + branch.** `sync_conflicts` rows are
|
|
110
|
-
recorded with the originating worktree path and current branch, so resolving a
|
|
111
|
-
conflict in one worktree never erases a peer worktree's conflict on the same
|
|
112
|
-
entity. `sync conflicts list` and `sync resolve` from a given worktree only
|
|
113
|
-
see and act on rows scoped to that worktree's branch.
|
|
114
|
-
|
|
115
|
-
## Worktree diagnostics and destructive scope
|
|
116
|
-
|
|
117
|
-
- Inspect machine-readable storage fields when debugging worktrees:
|
|
118
|
-
`storageMode`, `repoCommonDir`, `worktreeRoot`, `sharedStorageRoot`, and
|
|
119
|
-
`databaseFile`.
|
|
120
|
-
- `sharedStorageRoot` is the repo-scoped source of truth for `.trekoon` in git
|
|
121
|
-
worktrees.
|
|
122
|
-
- If `trekoon wipe --yes --toon` is explicitly requested, warn that it deletes
|
|
123
|
-
shared storage for the entire repository and every linked worktree.
|
|
124
|
-
- Wipe is destructive recovery only; it is never the right fix for a tracked DB
|
|
125
|
-
or gitignore mistake.
|
|
126
|
-
|
|
127
|
-
Trekoon stores local state in `.trekoon/trekoon.db`. In git repos and
|
|
128
|
-
worktrees, storage resolves from the shared repository root rather than each
|
|
129
|
-
worktree independently.
|
|
63
|
+
Use batch resolve only when the conflict pattern is uniform. Otherwise inspect
|
|
64
|
+
and resolve individually or narrow by `--entity` / `--field`.
|
|
65
|
+
|
|
66
|
+
## Worktree Scope
|
|
67
|
+
|
|
68
|
+
Inspect machine-readable storage fields when debugging worktrees:
|
|
69
|
+
`storageMode`, `repoCommonDir`, `worktreeRoot`, `sharedStorageRoot`, and
|
|
70
|
+
`databaseFile`.
|
|
71
|
+
|
|
72
|
+
Conflicts are scoped per worktree and branch. `sync conflicts list` and
|
|
73
|
+
`sync resolve` only act on rows for the current worktree/branch, so resolving a
|
|
74
|
+
conflict does not erase a peer worktree's conflict on the same entity.
|
|
75
|
+
|
|
76
|
+
## Destructive Recovery
|
|
77
|
+
|
|
78
|
+
`sharedStorageRoot` is the repo-scoped source of truth for `.trekoon` in git
|
|
79
|
+
worktrees. If the user explicitly requests `trekoon wipe --yes --toon`, warn
|
|
80
|
+
that it deletes shared storage for the whole repository and every linked
|
|
81
|
+
worktree. Wipe is destructive recovery only; it is never the fix for a tracked
|
|
82
|
+
DB or gitignore mistake.
|
package/README.md
CHANGED
|
@@ -90,8 +90,8 @@ What the agent does during execution:
|
|
|
90
90
|
calls `task done`
|
|
91
91
|
6. `task done` returns which downstream tasks just became unblocked, so the
|
|
92
92
|
orchestrator knows what to dispatch next
|
|
93
|
-
7. After all tasks complete:
|
|
94
|
-
the epic `done`
|
|
93
|
+
7. After all tasks complete: review agent or review skill for non-trivial
|
|
94
|
+
changes, tests, manual verification, then marks the epic `done`
|
|
95
95
|
|
|
96
96
|
The orchestrator uses `task done` responses to drive the whole loop. No polling,
|
|
97
97
|
no guessing. When a task finishes, Trekoon tells you exactly what's ready next.
|
|
@@ -110,13 +110,15 @@ agents don't know how to use Trekoon properly.
|
|
|
110
110
|
```bash
|
|
111
111
|
trekoon skills install # repo-local (.agents/skills/trekoon/)
|
|
112
112
|
trekoon skills install -g # global (~/.agents/skills/trekoon)
|
|
113
|
+
trekoon skills install --link --editor codex # editor link (.codex/skills/trekoon)
|
|
113
114
|
trekoon update # refresh all installed skills
|
|
114
115
|
```
|
|
115
116
|
|
|
116
|
-
The skill bundles
|
|
117
|
+
The skill bundles reference documents that agents load on demand:
|
|
117
118
|
|
|
118
119
|
| Agent needs to... | Skill reads | What it covers |
|
|
119
120
|
| --- | --- | --- |
|
|
121
|
+
| Coordinate across harnesses | `reference/harness-primitives.md` | Universal subagent, question, local task display, review, and evidence-recording guidance |
|
|
120
122
|
| Plan a feature | `reference/planning.md` | Decomposition, writing standard, dependency modeling, validation |
|
|
121
123
|
| Execute an epic | `reference/execution.md` | Graph building, lane grouping, sub-agent dispatch, verification (universal) |
|
|
122
124
|
| Execute with Agent Teams | `reference/execution-with-team.md` | TeamCreate/SendMessage, parallel Claude Code instances in tmux |
|
|
@@ -145,6 +147,16 @@ Execute only:
|
|
|
145
147
|
/trekoon <epic-id> execute
|
|
146
148
|
```
|
|
147
149
|
|
|
150
|
+
When you execute an epic, use subagents by default for any meaningful work that
|
|
151
|
+
can run independently. Keep small or tightly coupled tasks in the parent agent.
|
|
152
|
+
Use the main context for orchestration, dependency decisions, user
|
|
153
|
+
communication, and final synthesis while Trekoon remains the durable source of
|
|
154
|
+
truth.
|
|
155
|
+
|
|
156
|
+
If a harness has a higher-priority rule requiring explicit permission before
|
|
157
|
+
subagents, surface that immediately instead of silently doing broad work
|
|
158
|
+
yourself.
|
|
159
|
+
|
|
148
160
|
Plan and execute end to end:
|
|
149
161
|
|
|
150
162
|
```
|
|
@@ -154,9 +166,11 @@ dependency order until the epic is complete
|
|
|
154
166
|
|
|
155
167
|
## Agent Teams
|
|
156
168
|
|
|
157
|
-
For larger epics,
|
|
158
|
-
|
|
159
|
-
|
|
169
|
+
For larger epics, use the current harness's native subagent mechanism for
|
|
170
|
+
meaningful work that can run independently. Claude Code Agent Teams are one
|
|
171
|
+
runtime-specific option: instead of sequential sub-agents, you get real parallel
|
|
172
|
+
Claude Code instances coordinated through `TeamCreate` and `SendMessage`, each
|
|
173
|
+
running in its own tmux pane.
|
|
160
174
|
|
|
161
175
|
Requires Claude Code env variable `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=true`.
|
|
162
176
|
|
|
@@ -201,7 +215,7 @@ shell, worktree, or browser tab show up live without a manual refresh.
|
|
|
201
215
|
| --- | --- |
|
|
202
216
|
| Set up a repo | `trekoon init` |
|
|
203
217
|
| Open the local board | `trekoon board open` |
|
|
204
|
-
| Plan work | `trekoon epic create ...`, `trekoon epic expand ...` |
|
|
218
|
+
| Plan work | `trekoon --toon epic create ...`, `trekoon --toon epic expand ...` |
|
|
205
219
|
| Create tasks in bulk | `trekoon task create-many ...` |
|
|
206
220
|
| Add dependencies | `trekoon dep add-many ...` |
|
|
207
221
|
| Start an agent session | `trekoon session --epic <id>` |
|
package/docs/ai-agents.md
CHANGED
|
@@ -14,22 +14,39 @@ agent to:
|
|
|
14
14
|
- append progress and blocker notes instead of rewriting descriptions
|
|
15
15
|
- preview scoped replace before `--apply`
|
|
16
16
|
- treat `.trekoon` as shared repo-scoped state
|
|
17
|
+
- use harness-local todo/task tools as a live progress display while keeping
|
|
18
|
+
Trekoon as the durable source of truth
|
|
19
|
+
- use subagents by default for meaningful work that can run independently when
|
|
20
|
+
the harness exposes them
|
|
21
|
+
- keep small or tightly coupled tasks in the parent agent so the main context
|
|
22
|
+
stays available for orchestration and decisions
|
|
17
23
|
|
|
18
24
|
The skill ships with reference guides so the agent can handle the full
|
|
19
25
|
plan-to-completion workflow from one install:
|
|
20
26
|
|
|
21
27
|
```
|
|
22
28
|
.agents/skills/trekoon/
|
|
23
|
-
SKILL.md <- command
|
|
29
|
+
SKILL.md <- command router and operating contract
|
|
24
30
|
reference/
|
|
31
|
+
harness-primitives.md <- universal agent/task/question/review primitives
|
|
25
32
|
planning.md <- decomposition, writing standard, validation
|
|
26
33
|
execution.md <- graph building, lane dispatch, verification
|
|
27
34
|
execution-with-team.md <- Agent Teams pattern (Claude Code only)
|
|
28
35
|
```
|
|
29
36
|
|
|
30
|
-
The
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
The command shape determines what the agent loads:
|
|
38
|
+
|
|
39
|
+
| User command | Required references |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| `/trekoon plan <goal>` | `harness-primitives.md`, then `planning.md` |
|
|
42
|
+
| `/trekoon <id>` | `SKILL.md` only, unless deeper analysis is needed |
|
|
43
|
+
| `/trekoon <id> analyze` | `SKILL.md` plus targeted Trekoon reads |
|
|
44
|
+
| `/trekoon <id> execute` | `harness-primitives.md`, then `execution.md` |
|
|
45
|
+
| `/trekoon <id> team execute` | `harness-primitives.md`, then `execution-with-team.md` |
|
|
46
|
+
|
|
47
|
+
`harness-primitives.md` is loaded before planning or execution because those
|
|
48
|
+
modes may need structured questions, local progress displays, subagent
|
|
49
|
+
delegation, review agents, and runtime-specific orchestration.
|
|
33
50
|
|
|
34
51
|
## Install the skill
|
|
35
52
|
|
|
@@ -42,6 +59,7 @@ Create a project-local editor link when your agent environment supports it:
|
|
|
42
59
|
```bash
|
|
43
60
|
trekoon skills install --link --editor opencode
|
|
44
61
|
trekoon skills install --link --editor claude
|
|
62
|
+
trekoon skills install --link --editor codex
|
|
45
63
|
trekoon skills install --link --editor pi
|
|
46
64
|
trekoon skills install --link --editor opencode --to ./.custom-editor/skills
|
|
47
65
|
trekoon skills update
|
|
@@ -52,6 +70,7 @@ Path behavior:
|
|
|
52
70
|
- Canonical install: `.agents/skills/trekoon/SKILL.md`
|
|
53
71
|
- OpenCode link: `.opencode/skills/trekoon`
|
|
54
72
|
- Claude link: `.claude/skills/trekoon`
|
|
73
|
+
- Codex link: `.codex/skills/trekoon`
|
|
55
74
|
- Pi link: `.pi/skills/trekoon`
|
|
56
75
|
- `--to <path>` changes only the editor link root
|
|
57
76
|
- `--allow-outside-repo` is for intentional external links
|
|
@@ -97,6 +116,17 @@ Typical flow:
|
|
|
97
116
|
|
|
98
117
|
The core loop: **session, work, task done, repeat**.
|
|
99
118
|
|
|
119
|
+
When you execute an epic, use subagents by default for any meaningful work that
|
|
120
|
+
can run independently. Keep small or tightly coupled tasks in the parent agent.
|
|
121
|
+
Build the Trekoon execution graph, group ready tasks into lanes, keep a local
|
|
122
|
+
todo/task display for the user, and synthesize the results. Use the parent
|
|
123
|
+
context for dependency decisions and finishing the epic.
|
|
124
|
+
|
|
125
|
+
Some harnesses have higher-priority rules that require explicit user permission
|
|
126
|
+
before spawning subagents. When that happens, say so immediately after
|
|
127
|
+
discovering independent lanes and ask for permission before broad execution. Do
|
|
128
|
+
not silently default to single-agent execution.
|
|
129
|
+
|
|
100
130
|
Start with a single orientation call, optionally scoped to an epic:
|
|
101
131
|
|
|
102
132
|
```bash
|
|
@@ -193,6 +223,20 @@ notes, mark it done, and repeat until there are no ready tasks or you hit a
|
|
|
193
223
|
blocker.
|
|
194
224
|
```
|
|
195
225
|
|
|
226
|
+
### Execute with subagents where useful
|
|
227
|
+
|
|
228
|
+
```text
|
|
229
|
+
/trekoon <epic-id> execute -- use subagents by default for meaningful work that
|
|
230
|
+
can run independently, keep Trekoon as the source of truth, and use local task
|
|
231
|
+
tools to show live progress.
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Short form for harnesses that require explicit delegation wording:
|
|
235
|
+
|
|
236
|
+
```text
|
|
237
|
+
/trekoon <epic-id> execute with subagents
|
|
238
|
+
```
|
|
239
|
+
|
|
196
240
|
### Plan and execute end to end
|
|
197
241
|
|
|
198
242
|
```text
|
package/docs/commands.md
CHANGED
|
@@ -22,8 +22,8 @@ the quickest way to get started, read [Quickstart](quickstart.md) first.
|
|
|
22
22
|
- `trekoon sync resolve <conflict-id> --use ours|theirs [--dry-run]`
|
|
23
23
|
- `trekoon sync resolve --all --use ours|theirs [--entity <id>] [--field <name>] [--dry-run]`
|
|
24
24
|
- `trekoon sync conflicts <list|show> [--mode pending|all]`
|
|
25
|
-
- `trekoon skills install [--link --editor opencode|claude|pi] [--to <path>] [--allow-outside-repo]`
|
|
26
|
-
- `trekoon skills install -g|--global [--editor opencode|claude|pi]`
|
|
25
|
+
- `trekoon skills install [--link --editor opencode|claude|codex|pi] [--to <path>] [--allow-outside-repo]`
|
|
26
|
+
- `trekoon skills install -g|--global [--editor opencode|claude|codex|pi]`
|
|
27
27
|
- `trekoon skills update`
|
|
28
28
|
- `trekoon wipe --yes`
|
|
29
29
|
- `trekoon serve` (experimental)
|
package/docs/quickstart.md
CHANGED
|
@@ -69,7 +69,7 @@ if you just need to refresh the runtime assets without opening the browser.
|
|
|
69
69
|
## Create work
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
|
-
trekoon epic create --title "Agent backlog stabilization" --description "Track stabilization work" --status todo
|
|
72
|
+
trekoon --toon epic create --title "Agent backlog stabilization" --description "Track stabilization work" --status todo
|
|
73
73
|
trekoon task create --title "Implement sync status" --description "Add status reporting" --epic <epic-id> --status todo
|
|
74
74
|
trekoon subtask create --task <task-id> --title "Add cursor model" --status todo
|
|
75
75
|
```
|
|
@@ -88,7 +88,7 @@ trekoon task list --all --view compact
|
|
|
88
88
|
If you already know the full epic tree, create everything in one call:
|
|
89
89
|
|
|
90
90
|
```bash
|
|
91
|
-
trekoon epic create \
|
|
91
|
+
trekoon --toon epic create \
|
|
92
92
|
--title "Batch command rollout" \
|
|
93
93
|
--description "Ship one-shot planning workflows" \
|
|
94
94
|
--task "task-a|First task|First description|todo" \
|
|
@@ -118,7 +118,7 @@ For larger updates, use batch commands instead of looping:
|
|
|
118
118
|
| Multiple tasks under one epic | `trekoon task create-many --epic <epic-id> --task ...` |
|
|
119
119
|
| Multiple subtasks under one task | `trekoon subtask create-many <task-id> --subtask ...` |
|
|
120
120
|
| Multiple dependency edges | `trekoon dep add-many --dep ...` |
|
|
121
|
-
| Expand an existing epic | `trekoon epic expand <epic-id> ...` |
|
|
121
|
+
| Expand an existing epic | `trekoon --toon epic expand <epic-id> ...` |
|
|
122
122
|
|
|
123
123
|
These validate the whole batch before applying, so a bad input fails the entire
|
|
124
124
|
operation instead of leaving partial state.
|
package/package.json
CHANGED
package/src/board/assets/app.js
CHANGED
|
@@ -479,9 +479,11 @@ export async function bootLegacyBoard(options = {}) {
|
|
|
479
479
|
rerender,
|
|
480
480
|
});
|
|
481
481
|
if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
|
|
482
|
-
|
|
482
|
+
const disposeSnapshotSubscription = () => {
|
|
483
483
|
snapshotSubscription.dispose();
|
|
484
|
-
}
|
|
484
|
+
};
|
|
485
|
+
window.addEventListener("beforeunload", disposeSnapshotSubscription, { once: true });
|
|
486
|
+
window.addEventListener("pagehide", disposeSnapshotSubscription, { once: true });
|
|
485
487
|
}
|
|
486
488
|
|
|
487
489
|
// Actions for delegation
|
|
@@ -153,8 +153,8 @@ export function preserveFormState(container, writeFn, options = {}) {
|
|
|
153
153
|
|
|
154
154
|
// Per-form cache for getManagedControls: avoids the O(n^2) re-query that
|
|
155
155
|
// occurs when many controls share the same form root.
|
|
156
|
-
const
|
|
157
|
-
const inputs = getManagedControls(container,
|
|
156
|
+
const controlCache = new Map();
|
|
157
|
+
const inputs = getManagedControls(container, controlCache);
|
|
158
158
|
|
|
159
159
|
const activeElement = document.activeElement;
|
|
160
160
|
let focusedIdentity = null;
|
|
@@ -162,7 +162,7 @@ export function preserveFormState(container, writeFn, options = {}) {
|
|
|
162
162
|
const savedStates = inputs.map((el) => {
|
|
163
163
|
const form = getFormRoot(el);
|
|
164
164
|
const formId = getNamespacedFormIdentity(form);
|
|
165
|
-
const controlId = form ? getControlIdentity(el, form,
|
|
165
|
+
const controlId = form ? getControlIdentity(el, form, controlCache) : null;
|
|
166
166
|
const identity = controlId ? { formId, controlId } : null;
|
|
167
167
|
|
|
168
168
|
if (activeElement === el) {
|
|
@@ -180,6 +180,7 @@ export function preserveFormState(container, writeFn, options = {}) {
|
|
|
180
180
|
}).filter(s => s.identity);
|
|
181
181
|
|
|
182
182
|
writeFn();
|
|
183
|
+
controlCache.clear();
|
|
183
184
|
|
|
184
185
|
const formsByIdentity = new Map(
|
|
185
186
|
Array.from(container.querySelectorAll(FORM_ROOT_SELECTOR)).map((form) => [
|
|
@@ -188,16 +189,13 @@ export function preserveFormState(container, writeFn, options = {}) {
|
|
|
188
189
|
]),
|
|
189
190
|
);
|
|
190
191
|
|
|
191
|
-
// Fresh cache for the restore pass (DOM was replaced by writeFn).
|
|
192
|
-
const restoreCache = new Map();
|
|
193
|
-
|
|
194
192
|
for (const state of savedStates) {
|
|
195
193
|
const { formId, controlId } = state.identity;
|
|
196
194
|
if (resetFormIds.has(formId)) {
|
|
197
195
|
continue;
|
|
198
196
|
}
|
|
199
197
|
const form = formsByIdentity.get(formId) ?? container;
|
|
200
|
-
const restored = getManagedControls(form,
|
|
198
|
+
const restored = getManagedControls(form, controlCache).find((control) => getControlIdentity(control, form, controlCache) === controlId);
|
|
201
199
|
if (restored && restored.value !== state.value) {
|
|
202
200
|
restored.value = state.value;
|
|
203
201
|
}
|
|
@@ -213,7 +211,7 @@ export function preserveFormState(container, writeFn, options = {}) {
|
|
|
213
211
|
return;
|
|
214
212
|
}
|
|
215
213
|
const form = formsByIdentity.get(formId) ?? container;
|
|
216
|
-
const restored = getManagedControls(form,
|
|
214
|
+
const restored = getManagedControls(form, controlCache).find((control) => getControlIdentity(control, form, controlCache) === controlId);
|
|
217
215
|
if (restored) {
|
|
218
216
|
restored.focus({ preventScroll: true });
|
|
219
217
|
const focusedState = savedStates.find((state) => state.identity?.formId === formId && state.identity?.controlId === controlId);
|
|
@@ -444,6 +444,9 @@ export function createBoardActions(options) {
|
|
|
444
444
|
const nextKey = feedback ? `${feedback.targetStatus}|${feedback.kind}` : null;
|
|
445
445
|
if (prevKey === nextKey) return;
|
|
446
446
|
store.dragFeedback = feedback;
|
|
447
|
+
if (typeof model.invalidateBoardStateMemo === "function") {
|
|
448
|
+
model.invalidateBoardStateMemo();
|
|
449
|
+
}
|
|
447
450
|
rerender();
|
|
448
451
|
},
|
|
449
452
|
dropTaskStatus(taskId, nextStatus) {
|
|
@@ -222,10 +222,9 @@ function createTimeoutError(method, path, timeoutMs) {
|
|
|
222
222
|
* }}
|
|
223
223
|
*/
|
|
224
224
|
export function createMutationQueue(model, rerender) {
|
|
225
|
-
/** @type {Array<{ optimistic?: function, request: function, onSuccess?: function, onError?: function, successMessage?: string }>} */
|
|
225
|
+
/** @type {Array<{ mutationId: string, optimistic?: function, request: function, onSuccess?: function, onError?: function, successMessage?: string }>} */
|
|
226
226
|
const queue = [];
|
|
227
227
|
let processing = false;
|
|
228
|
-
let nextMutationId = 1;
|
|
229
228
|
/** @type {Array<() => void>} */
|
|
230
229
|
let flushResolvers = [];
|
|
231
230
|
|
|
@@ -246,7 +245,7 @@ export function createMutationQueue(model, rerender) {
|
|
|
246
245
|
|
|
247
246
|
while (queue.length > 0) {
|
|
248
247
|
const mutation = queue.shift();
|
|
249
|
-
if (model.store.notice?.retryMutationId !== mutation.
|
|
248
|
+
if (model.store.notice?.retryMutationId !== mutation.mutationId) {
|
|
250
249
|
model.store.notice = null;
|
|
251
250
|
}
|
|
252
251
|
|
|
@@ -258,8 +257,8 @@ export function createMutationQueue(model, rerender) {
|
|
|
258
257
|
|
|
259
258
|
try {
|
|
260
259
|
if (typeof mutation.optimistic === "function") {
|
|
261
|
-
const previousSnapshot =
|
|
262
|
-
const optimisticSnapshot = mutation.optimistic(cloneSnapshot(
|
|
260
|
+
const previousSnapshot = model.store.snapshot;
|
|
261
|
+
const optimisticSnapshot = mutation.optimistic(cloneSnapshot(previousSnapshot));
|
|
263
262
|
inverseDelta = computeInverseDelta(previousSnapshot, optimisticSnapshot);
|
|
264
263
|
model.store.snapshot = optimisticSnapshot;
|
|
265
264
|
// Direct snapshot mutation bypasses setState/syncState; invalidate
|
|
@@ -298,7 +297,7 @@ export function createMutationQueue(model, rerender) {
|
|
|
298
297
|
title: "Action failed",
|
|
299
298
|
message,
|
|
300
299
|
retryLabel: "Retry",
|
|
301
|
-
retryMutationId: mutation.
|
|
300
|
+
retryMutationId: mutation.mutationId,
|
|
302
301
|
};
|
|
303
302
|
|
|
304
303
|
if (typeof mutation.onError === "function") {
|
|
@@ -318,8 +317,10 @@ export function createMutationQueue(model, rerender) {
|
|
|
318
317
|
|
|
319
318
|
return {
|
|
320
319
|
enqueue(mutation) {
|
|
321
|
-
queue.push({
|
|
322
|
-
|
|
320
|
+
queue.push({
|
|
321
|
+
...mutation,
|
|
322
|
+
mutationId: mutation.mutationId ?? crypto.randomUUID(),
|
|
323
|
+
});
|
|
323
324
|
processNext();
|
|
324
325
|
},
|
|
325
326
|
|
|
@@ -569,7 +570,7 @@ export function createApi(model, options) {
|
|
|
569
570
|
*
|
|
570
571
|
* @param {object} model - Store with `applySnapshotDelta` method
|
|
571
572
|
* @param {object} options
|
|
572
|
-
* @param {string} options.sessionToken - Auth token
|
|
573
|
+
* @param {string} options.sessionToken - Auth token for API parity; EventSource uses the same-origin HttpOnly cookie.
|
|
573
574
|
* @param {function} options.rerender - Trigger UI rerender after applying deltas
|
|
574
575
|
* @param {typeof EventSource} [options.EventSourceCtor] - Constructor override for tests
|
|
575
576
|
* @param {string} [options.path] - Override stream path; default /api/snapshot/stream
|
|
@@ -577,7 +578,6 @@ export function createApi(model, options) {
|
|
|
577
578
|
*/
|
|
578
579
|
export function subscribeSnapshotStream(model, options) {
|
|
579
580
|
const {
|
|
580
|
-
sessionToken,
|
|
581
581
|
rerender,
|
|
582
582
|
EventSourceCtor = typeof EventSource !== "undefined" ? EventSource : null,
|
|
583
583
|
path = "/api/snapshot/stream",
|
|
@@ -587,14 +587,20 @@ export function subscribeSnapshotStream(model, options) {
|
|
|
587
587
|
return { dispose: () => {}, eventSource: null };
|
|
588
588
|
}
|
|
589
589
|
|
|
590
|
-
// EventSource cannot set custom headers, so the auth token rides as a query
|
|
591
|
-
// parameter. Server `extractToken` already accepts ?token=.
|
|
592
|
-
const url = sessionToken && sessionToken.length > 0
|
|
593
|
-
? `${path}?token=${encodeURIComponent(sessionToken)}`
|
|
594
|
-
: path;
|
|
595
|
-
|
|
596
590
|
let disposed = false;
|
|
597
|
-
|
|
591
|
+
let consecutiveErrors = 0;
|
|
592
|
+
const eventSource = new EventSourceCtor(path);
|
|
593
|
+
|
|
594
|
+
const clearLiveUpdateNotice = () => {
|
|
595
|
+
if (model.store?.notice?.code === "live_updates_disconnected") {
|
|
596
|
+
model.store.notice = null;
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
const markLiveUpdateSuccess = () => {
|
|
601
|
+
consecutiveErrors = 0;
|
|
602
|
+
clearLiveUpdateNotice();
|
|
603
|
+
};
|
|
598
604
|
|
|
599
605
|
const handleSnapshotDelta = (event) => {
|
|
600
606
|
if (disposed) return;
|
|
@@ -613,6 +619,7 @@ export function subscribeSnapshotStream(model, options) {
|
|
|
613
619
|
const delta = payload?.snapshotDelta;
|
|
614
620
|
if (!delta || typeof delta !== "object") return;
|
|
615
621
|
model.applySnapshotDelta(delta);
|
|
622
|
+
markLiveUpdateSuccess();
|
|
616
623
|
if (typeof rerender === "function") rerender();
|
|
617
624
|
};
|
|
618
625
|
|
|
@@ -632,27 +639,42 @@ export function subscribeSnapshotStream(model, options) {
|
|
|
632
639
|
if (!snapshot || typeof snapshot !== "object") return;
|
|
633
640
|
if (typeof model.replaceSnapshot === "function") {
|
|
634
641
|
model.replaceSnapshot(snapshot);
|
|
642
|
+
markLiveUpdateSuccess();
|
|
635
643
|
if (typeof rerender === "function") rerender();
|
|
636
644
|
}
|
|
637
645
|
};
|
|
638
646
|
|
|
647
|
+
function dispose() {
|
|
648
|
+
if (disposed) return;
|
|
649
|
+
disposed = true;
|
|
650
|
+
try {
|
|
651
|
+
eventSource.close();
|
|
652
|
+
} catch {
|
|
653
|
+
// best-effort
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
639
657
|
const handleError = () => {
|
|
640
658
|
if (disposed) return;
|
|
641
|
-
|
|
642
|
-
// updates. EventSource will keep auto-reconnecting; once a snapshot/
|
|
643
|
-
// snapshotDelta event lands again, the regular notice clearing flow
|
|
644
|
-
// (e.g. on the next mutation) replaces this notice.
|
|
659
|
+
consecutiveErrors += 1;
|
|
645
660
|
if (model.store && typeof model.store === "object") {
|
|
646
661
|
const existing = model.store.notice;
|
|
647
|
-
|
|
662
|
+
const disabled = consecutiveErrors >= 5;
|
|
663
|
+
const nextCode = disabled ? "live_updates_disabled" : "live_updates_disconnected";
|
|
664
|
+
if (!existing || existing.code !== nextCode) {
|
|
648
665
|
model.store.notice = {
|
|
649
666
|
type: "warning",
|
|
650
|
-
code:
|
|
651
|
-
title: "Live updates disconnected",
|
|
652
|
-
message:
|
|
667
|
+
code: nextCode,
|
|
668
|
+
title: disabled ? "Live updates disabled" : "Live updates disconnected",
|
|
669
|
+
message: disabled
|
|
670
|
+
? "Refresh the board to resume live updates from other sessions."
|
|
671
|
+
: "Reconnecting to the server. Changes from other sessions may be delayed.",
|
|
653
672
|
};
|
|
654
673
|
if (typeof rerender === "function") rerender();
|
|
655
674
|
}
|
|
675
|
+
if (disabled) {
|
|
676
|
+
dispose();
|
|
677
|
+
}
|
|
656
678
|
}
|
|
657
679
|
};
|
|
658
680
|
|
|
@@ -665,14 +687,6 @@ export function subscribeSnapshotStream(model, options) {
|
|
|
665
687
|
|
|
666
688
|
return {
|
|
667
689
|
eventSource,
|
|
668
|
-
dispose
|
|
669
|
-
if (disposed) return;
|
|
670
|
-
disposed = true;
|
|
671
|
-
try {
|
|
672
|
-
eventSource.close();
|
|
673
|
-
} catch {
|
|
674
|
-
// best-effort
|
|
675
|
-
}
|
|
676
|
-
},
|
|
690
|
+
dispose,
|
|
677
691
|
};
|
|
678
692
|
}
|