feinai 0.6.6 → 0.6.7
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/README.md +7 -9
- package/package.json +1 -1
- package/skills/feinai-write-spec/SKILL.md +130 -25
- package/skills/feinai-write-tasks/SKILL.md +1 -1
- package/src/cli.ts +1 -1
- package/src/dashboard.html +112 -23
package/README.md
CHANGED
|
@@ -111,15 +111,13 @@ Agents don't parse `QUEUE.md`. They talk to a small local coordination service i
|
|
|
111
111
|
|
|
112
112
|
## Claude Code skills
|
|
113
113
|
|
|
114
|
-
feinai ships
|
|
114
|
+
feinai ships three Claude Code skills covering the full development loop. They activate automatically when `.feinai/feinai.db` is present:
|
|
115
115
|
|
|
116
|
-
| Skill | Purpose
|
|
117
|
-
|
|
118
|
-
| `feinai-
|
|
119
|
-
| `feinai-
|
|
120
|
-
| `feinai-
|
|
121
|
-
| `feinai-dispatch` | Orchestrates subagents in isolated git worktrees |
|
|
122
|
-
| `feinai-implement` | Claims and executes one task end‑to‑end |
|
|
116
|
+
| Skill | Purpose |
|
|
117
|
+
|---------------------|----------------------------------------------------------------------------------|
|
|
118
|
+
| `feinai-write-spec` | Full pipeline: spec + plan + tasks. With no argument runs the complete flow; with a SPEC-ID argument regenerates tasks only (use when iterating on an existing plan) |
|
|
119
|
+
| `feinai-dispatch` | Orchestrates subagents in isolated git worktrees |
|
|
120
|
+
| `feinai-implement` | Claims and executes one task end‑to‑end |
|
|
123
121
|
|
|
124
122
|
Together they cover: design → spec → plan → tasks → parallel execution → merge.
|
|
125
123
|
|
|
@@ -129,7 +127,7 @@ Together they cover: design → spec → plan → tasks → parallel execution
|
|
|
129
127
|
mkdir -p ~/.claude/skills
|
|
130
128
|
SKILLS=~/.bun/install/global/node_modules/feinai/skills
|
|
131
129
|
|
|
132
|
-
for skill in feinai-
|
|
130
|
+
for skill in feinai-write-spec feinai-dispatch feinai-implement; do
|
|
133
131
|
ln -sf "$SKILLS/$skill" ~/.claude/skills/$skill
|
|
134
132
|
done
|
|
135
133
|
```
|
package/package.json
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: feinai-write-spec
|
|
3
|
-
description: Use when the user wants to design a new feature, refactor, or change in a project that has `.feinai/feinai.db`.
|
|
3
|
+
description: Use when the user wants to design a new feature, refactor, or change in a project that has `.feinai/feinai.db`. With no argument, runs the full pipeline (spec + plan + tasks). With a SPEC-ID argument, skips to task generation only (use when iterating on an existing plan). Replaces `brainstorming` + `writing-plans` + `feinai-write-tasks` from superpowers when feinai is active.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# feinai-write-spec
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Full pipeline: spec → plan → tasks. One skill, two entry points.
|
|
9
|
+
|
|
10
|
+
## Entry point detection
|
|
11
|
+
|
|
12
|
+
**Check the first argument before doing anything else.**
|
|
13
|
+
|
|
14
|
+
- **No argument** → full pipeline (Phases 1–5)
|
|
15
|
+
- **SPEC-ID given** (e.g. `SPEC-42`) → tasks only, jump to **Phase 4**
|
|
16
|
+
|
|
17
|
+
Ask the user which mode only if ambiguous.
|
|
18
|
+
|
|
19
|
+
---
|
|
9
20
|
|
|
10
21
|
## Preconditions
|
|
11
22
|
|
|
12
23
|
Run `feinai status` (exit 0 = feinai is active). If not active, stop and ask the
|
|
13
24
|
user if they want `feinai init` or to fall back to vanilla superpowers.
|
|
14
25
|
|
|
15
|
-
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Phase 1 — Decide entry mode (full pipeline only)
|
|
16
29
|
|
|
17
30
|
Decide which entry mode applies BEFORE doing anything else. Ask the user only if ambiguous.
|
|
18
31
|
|
|
@@ -31,7 +44,7 @@ as a minimal change (just the input + endpoint call) or shall I think wider
|
|
|
31
44
|
|
|
32
45
|
---
|
|
33
46
|
|
|
34
|
-
## Phase
|
|
47
|
+
## Phase 2 — Load project context (full pipeline only)
|
|
35
48
|
|
|
36
49
|
Read **in this order, stop early if enough**:
|
|
37
50
|
|
|
@@ -50,9 +63,9 @@ Keep this phase tight — every read costs tokens. Stop as soon as you can write
|
|
|
50
63
|
|
|
51
64
|
---
|
|
52
65
|
|
|
53
|
-
## Phase
|
|
66
|
+
## Phase 3 — Draft and write the spec + plan (full pipeline only)
|
|
54
67
|
|
|
55
|
-
|
|
68
|
+
### Spec (what and why — not how)
|
|
56
69
|
|
|
57
70
|
Required sections:
|
|
58
71
|
- **Goal** — one sentence
|
|
@@ -90,19 +103,14 @@ feinai spec add SPEC-NNN "Short title" --stdin <<'FEINAI_EOF'
|
|
|
90
103
|
FEINAI_EOF
|
|
91
104
|
```
|
|
92
105
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
## Phase 3 — Draft and write the plan
|
|
96
|
-
|
|
97
|
-
**Plan answers: how.** Architecture decisions, file map, dependencies.
|
|
106
|
+
### Plan (how — architecture decisions, file map, dependencies)
|
|
98
107
|
|
|
99
108
|
Required sections:
|
|
100
109
|
- **Architecture overview** — 2–4 sentences
|
|
101
110
|
- **Files to touch** — explicit list
|
|
102
|
-
- **Task breakdown preview** — high-level (the actual tasks
|
|
111
|
+
- **Task breakdown preview** — high-level (the actual tasks come in Phase 4)
|
|
103
112
|
- **Quality gates** — the commands that prove correctness
|
|
104
113
|
|
|
105
|
-
**Write the plan:**
|
|
106
114
|
```bash
|
|
107
115
|
feinai plan add SPEC-NNN --stdin <<'FEINAI_EOF'
|
|
108
116
|
# Plan v1 — SPEC-NNN: Title
|
|
@@ -127,19 +135,112 @@ FEINAI_EOF
|
|
|
127
135
|
|
|
128
136
|
---
|
|
129
137
|
|
|
130
|
-
## Phase 4 —
|
|
138
|
+
## Phase 4 — Build the task graph
|
|
131
139
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
140
|
+
*Entry point when SPEC-ID is given as argument.*
|
|
141
|
+
|
|
142
|
+
### Step 1 — Load spec + plan
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
feinai spec show SPEC-NNN --full
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Read both. Internalize:
|
|
149
|
+
- The WHAT (spec)
|
|
150
|
+
- The HOW (plan)
|
|
151
|
+
- The "Files to touch" list
|
|
152
|
+
- The "Task breakdown preview" — your starting point, not a contract
|
|
153
|
+
|
|
154
|
+
### Step 2 — Analyze granularity and parallelism
|
|
155
|
+
|
|
156
|
+
**A. Granularity** — one task = one logical change a single subagent can complete in one session.
|
|
157
|
+
- Too big: "implement the entire auth system" → split by route
|
|
158
|
+
- Too small: "add a single import" → merge into larger task
|
|
159
|
+
- Sweet spot: ~50–300 lines, 1–3 files
|
|
160
|
+
|
|
161
|
+
**B. Parallelism rule — no exceptions:**
|
|
162
|
+
|
|
163
|
+
> **Tasks that touch the same file cannot run in parallel.**
|
|
164
|
+
|
|
165
|
+
When a **shared file** needs changes (e.g. `router.ts`, `index.ts`, schema), extract it first:
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
TASK-NNN-0: edit shared file once
|
|
169
|
+
↓ blocks
|
|
170
|
+
TASK-NNN-1: feature A ┐
|
|
171
|
+
TASK-NNN-2: feature B ├ parallel
|
|
172
|
+
TASK-NNN-3: feature C ┘
|
|
173
|
+
```
|
|
135
174
|
|
|
136
|
-
|
|
175
|
+
**C. Dependencies** — use `--blocked-by` for:
|
|
176
|
+
- File-level conflicts (above)
|
|
177
|
+
- Logical dependencies (B uses a type defined in A)
|
|
178
|
+
|
|
179
|
+
### Step 3 — Embed TDD instructions
|
|
180
|
+
|
|
181
|
+
Each implementation task description starts with:
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
## TDD baseline
|
|
185
|
+
Write the tests first based on the "Tests required" section of SPEC-NNN.
|
|
186
|
+
Run them — they must fail (the implementation doesn't exist yet).
|
|
187
|
+
Implement until all tests pass. Then run the quality gates.
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
No separate "write tests" tasks. Test + implementation live together.
|
|
191
|
+
|
|
192
|
+
**Exception:** if shared fixtures are needed across tasks, extract them into a tiny TASK-NNN-0 that others block on.
|
|
193
|
+
|
|
194
|
+
### Step 4 — Write the tasks
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
feinai add TASK-NNN-X "subject" \
|
|
198
|
+
--spec SPEC-NNN \
|
|
199
|
+
--desc "$(cat <<'EOF'
|
|
200
|
+
## TDD baseline
|
|
201
|
+
Write the tests first based on the "Tests required" section of SPEC-NNN.
|
|
202
|
+
Run them — they must fail. Implement until they pass.
|
|
203
|
+
|
|
204
|
+
## Files to touch
|
|
205
|
+
- packages/X/...
|
|
206
|
+
- packages/Y/...
|
|
207
|
+
|
|
208
|
+
## Implementation notes
|
|
209
|
+
<concrete, copy-pasteable details. Cite line numbers if useful.>
|
|
210
|
+
|
|
211
|
+
## Do not touch
|
|
212
|
+
- <files explicitly out of scope>
|
|
213
|
+
EOF
|
|
214
|
+
)" \
|
|
215
|
+
--package "@scope/pkg" \
|
|
216
|
+
--gate "pnpm --filter @scope/pkg typecheck" \
|
|
217
|
+
--gate "pnpm --filter @scope/pkg test -- --run" \
|
|
218
|
+
--blocked-by TASK-NNN-Y # repeatable if needed
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Task description = self-contained.** The subagent reads only the task (via `feinai take`) and gets spec+plan as `spec_context` automatically.
|
|
137
222
|
|
|
138
223
|
---
|
|
139
224
|
|
|
140
|
-
##
|
|
225
|
+
## Phase 5 — Hand off
|
|
226
|
+
|
|
227
|
+
Output a parallelism summary table:
|
|
228
|
+
|
|
229
|
+
```
|
|
230
|
+
TASK-NNN-0: shared file edit (sequential)
|
|
231
|
+
TASK-NNN-1: feature A (parallel with 2, 3) blocked-by 0
|
|
232
|
+
TASK-NNN-2: feature B (parallel with 1, 3) blocked-by 0
|
|
233
|
+
TASK-NNN-3: feature C (parallel with 1, 2) blocked-by 0
|
|
234
|
+
TASK-NNN-4: integration tests (sequential) blocked-by 1, 2, 3
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Tell the user:
|
|
238
|
+
> Tasks written for SPEC-NNN. View with `feinai list --spec SPEC-NNN`.
|
|
239
|
+
> Next step: run `/feinai-dispatch SPEC-NNN` to execute.
|
|
240
|
+
|
|
241
|
+
---
|
|
141
242
|
|
|
142
|
-
|
|
243
|
+
## Key questions to ask (inline, only if needed)
|
|
143
244
|
|
|
144
245
|
- Ambiguity in scope: *"Do we include X in this spec or punt to a follow-up?"*
|
|
145
246
|
- Missing architecture decision: *"This needs a choice between A and B. Default is A unless you say otherwise."*
|
|
@@ -152,18 +253,19 @@ These come up mid-session. Ask them inline, one at a time, brief:
|
|
|
152
253
|
## What NOT to do
|
|
153
254
|
|
|
154
255
|
- ❌ Write `docs/superpowers/specs/*.md` files — spec lives in feinai
|
|
155
|
-
- ❌ Create tasks in this skill — that's `feinai-write-tasks`
|
|
156
256
|
- ❌ Invent architecture if `ARCHITECTURE.md` is silent — ask the user
|
|
157
257
|
- ❌ Write spec content that is actually a plan (HOW). Keep them separated.
|
|
158
|
-
- ❌ Ask the user to confirm before writing — write
|
|
258
|
+
- ❌ Ask the user to confirm before writing — write spec/plan/tasks, then they review
|
|
259
|
+
- ❌ Re-think architecture in Phase 4 — it's in the plan, follow it
|
|
260
|
+
- ❌ Write tasks without a SPEC-ID — every task must have `--spec SPEC-NNN`
|
|
261
|
+
- ❌ Skip the same-file analysis — it's the single biggest cause of merge conflicts
|
|
262
|
+
- ❌ Hardcode worktree paths — `feinai-dispatch` assigns those at execution time
|
|
159
263
|
|
|
160
264
|
---
|
|
161
265
|
|
|
162
266
|
## Subagent isolation
|
|
163
267
|
|
|
164
|
-
If you delegate
|
|
165
|
-
the subagent must NOT use this skill — it has no context. Hand it concrete
|
|
166
|
-
findings and let it return text; you write to feinai yourself.
|
|
268
|
+
If you delegate spec drafting to a subagent, the subagent must NOT use this skill. Hand it concrete findings and let it return text; you write to feinai yourself.
|
|
167
269
|
|
|
168
270
|
---
|
|
169
271
|
|
|
@@ -176,3 +278,6 @@ findings and let it return text; you write to feinai yourself.
|
|
|
176
278
|
| Update spec content | `feinai spec set-content SPEC-N --stdin <<<` content |
|
|
177
279
|
| Write plan | `feinai plan add SPEC-N --stdin <<<` content |
|
|
178
280
|
| Show spec+plan together | `feinai spec show SPEC-N --full` |
|
|
281
|
+
| List tasks for spec | `feinai list --spec SPEC-N` |
|
|
282
|
+
| Add task | `feinai add TASK-X "subject" --spec SPEC-N --desc "..." --gate "..." [--blocked-by TASK-Y]` |
|
|
283
|
+
| Edit task | `feinai task edit TASK-X --desc "..." --gate "..."` |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: feinai-write-tasks
|
|
3
|
-
description: Use when a spec + plan already exist in feinai (written by `feinai-write-spec` or manually) and need to be broken down into executable tasks. Decomposes the plan into atomic `feinai add` calls, analyzes file-level parallelism, and embeds TDD instructions. Output: a set of tasks with `blocked_by` dependencies ready for `feinai-dispatch` to execute.
|
|
3
|
+
description: "Use when a spec + plan already exist in feinai (written by `feinai-write-spec` or manually) and need to be broken down into executable tasks. Decomposes the plan into atomic `feinai add` calls, analyzes file-level parallelism, and embeds TDD instructions. Output: a set of tasks with `blocked_by` dependencies ready for `feinai-dispatch` to execute."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# feinai-write-tasks
|
package/src/cli.ts
CHANGED
package/src/dashboard.html
CHANGED
|
@@ -293,6 +293,11 @@
|
|
|
293
293
|
z-index: 100;
|
|
294
294
|
}
|
|
295
295
|
.modal-bg.visible { display: flex; }
|
|
296
|
+
@keyframes modal-in {
|
|
297
|
+
from { opacity: 0; transform: scale(0.96); }
|
|
298
|
+
to { opacity: 1; transform: scale(1); }
|
|
299
|
+
}
|
|
300
|
+
.modal-bg.visible .modal { animation: modal-in 0.15s ease-out; }
|
|
296
301
|
.modal {
|
|
297
302
|
background: var(--bg-card);
|
|
298
303
|
border: 1px solid var(--border-strong);
|
|
@@ -389,6 +394,13 @@
|
|
|
389
394
|
.markdown em { color: var(--fg); font-style: italic; }
|
|
390
395
|
.events-list .item { padding: 8px 16px; cursor: default; }
|
|
391
396
|
.events-list .item:hover { background: transparent; }
|
|
397
|
+
.events-list .item.event-taken { border-left: 2px solid var(--blue); }
|
|
398
|
+
.events-list .item.event-completed { border-left: 2px solid var(--green); }
|
|
399
|
+
.events-list .item.event-failed { border-left: 2px solid var(--red); }
|
|
400
|
+
.events-list .item.event-created { border-left: 2px solid var(--accent); }
|
|
401
|
+
.events-list .item.event-released { border-left: 2px solid var(--gray); }
|
|
402
|
+
.events-list .item.event-started { border-left: 2px solid var(--yellow); }
|
|
403
|
+
.event-payload { font-size:11px;color:var(--fg-dim);background:var(--bg-input);border-radius:4px;padding:6px 8px;margin:4px 0 0;overflow:auto;max-height:120px; }
|
|
392
404
|
.event-type {
|
|
393
405
|
font-size: 11px;
|
|
394
406
|
padding: 1px 6px;
|
|
@@ -506,10 +518,17 @@
|
|
|
506
518
|
.worktree-card {
|
|
507
519
|
background: var(--bg-card);
|
|
508
520
|
border: 1px solid var(--border);
|
|
521
|
+
border-left: 3px solid transparent;
|
|
509
522
|
border-radius: 8px;
|
|
510
523
|
padding: 12px 14px;
|
|
511
524
|
font-size: 12px;
|
|
512
525
|
}
|
|
526
|
+
.worktree-card[data-card-state="active"] { border-left-color: var(--blue); }
|
|
527
|
+
.worktree-card[data-card-state="clean"] { border-left-color: var(--gray); }
|
|
528
|
+
.worktree-card[data-card-state="error"],
|
|
529
|
+
.worktree-card[data-card-state="removed"] { border-left-color: var(--red); }
|
|
530
|
+
.worktree-card[data-card-state="merged"] { border-left-color: var(--green); }
|
|
531
|
+
.worktree-card[data-card-state="not-created"] { border-left-color: var(--yellow); }
|
|
513
532
|
.worktree-card-header {
|
|
514
533
|
display: flex;
|
|
515
534
|
align-items: center;
|
|
@@ -666,6 +685,15 @@
|
|
|
666
685
|
font-size: 13px;
|
|
667
686
|
padding: 12px 0;
|
|
668
687
|
}
|
|
688
|
+
.modal-meta-grid { display: grid; grid-template-columns: 120px 1fr; gap: 4px 12px; font-size: 12px; margin: 8px 0; }
|
|
689
|
+
.modal-meta-label { color: var(--fg-faint); text-transform: uppercase; letter-spacing: 0.3px; font-size: 11px; padding-top: 2px; }
|
|
690
|
+
.modal-meta-value { color: var(--fg-dim); word-break: break-all; }
|
|
691
|
+
.modal-divider { border: none; border-top: 1px solid var(--border); margin: 12px 0; }
|
|
692
|
+
.confirm-overlay { position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:9999;display:flex;align-items:center;justify-content:center; }
|
|
693
|
+
.confirm-box { background:var(--bg-card);border:1px solid var(--border-strong);border-radius:10px;padding:24px;min-width:280px;max-width:380px; }
|
|
694
|
+
.confirm-message { margin:0 0 16px;font-size:15px; }
|
|
695
|
+
.confirm-skip { display:flex;align-items:center;gap:8px;font-size:12px;color:var(--fg-dim);margin-bottom:16px;cursor:pointer; }
|
|
696
|
+
.confirm-actions { display:flex;gap:8px;justify-content:flex-end; }
|
|
669
697
|
</style>
|
|
670
698
|
</head>
|
|
671
699
|
<body>
|
|
@@ -719,6 +747,11 @@
|
|
|
719
747
|
|
|
720
748
|
<section class="section-wide">
|
|
721
749
|
<h2>Recent events <span class="badge" id="event-count">0</span></h2>
|
|
750
|
+
<div class="filter-bar" id="event-filters">
|
|
751
|
+
<button class="filter-btn active" data-event-filter="">all</button>
|
|
752
|
+
<button class="filter-btn" data-event-filter="event">events</button>
|
|
753
|
+
<button class="filter-btn" data-event-filter="request">requests</button>
|
|
754
|
+
</div>
|
|
722
755
|
<div class="list events-list" id="events-list"><div class="empty">loading...</div></div>
|
|
723
756
|
</section>
|
|
724
757
|
</main>
|
|
@@ -745,6 +778,16 @@
|
|
|
745
778
|
const $$ = (sel) => Array.from(document.querySelectorAll(sel));
|
|
746
779
|
let activeTaskStatuses = new Set();
|
|
747
780
|
let activeSpecStatuses = new Set();
|
|
781
|
+
let activeEventFilter = '';
|
|
782
|
+
let lastDbEvents = [];
|
|
783
|
+
|
|
784
|
+
function relativeTime(dateStr) {
|
|
785
|
+
const diff = Date.now() - new Date(dateStr).getTime();
|
|
786
|
+
if (diff < 60000) return 'just now';
|
|
787
|
+
if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
|
|
788
|
+
if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago';
|
|
789
|
+
return new Date(dateStr).toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'});
|
|
790
|
+
}
|
|
748
791
|
let liveRequests = [];
|
|
749
792
|
|
|
750
793
|
// -------- strings --------
|
|
@@ -786,14 +829,14 @@
|
|
|
786
829
|
return new Promise((resolve) => {
|
|
787
830
|
if (localStorage.getItem("feinai-skip-" + skipKey) === "1") { resolve(true); return; }
|
|
788
831
|
const overlay = document.createElement("div");
|
|
789
|
-
overlay.
|
|
832
|
+
overlay.className = "confirm-overlay";
|
|
790
833
|
overlay.innerHTML = `
|
|
791
|
-
<div
|
|
792
|
-
<p
|
|
793
|
-
<label
|
|
834
|
+
<div class="confirm-box">
|
|
835
|
+
<p class="confirm-message">${escapeHtml(message)}</p>
|
|
836
|
+
<label class="confirm-skip">
|
|
794
837
|
<input type="checkbox" id="skip-cb"> ${t("skip_confirm")}
|
|
795
838
|
</label>
|
|
796
|
-
<div
|
|
839
|
+
<div class="confirm-actions">
|
|
797
840
|
<button id="confirm-cancel" class="btn">${t("cancel_btn")}</button>
|
|
798
841
|
<button id="confirm-ok" class="btn primary">${t("confirm_btn")}</button>
|
|
799
842
|
</div>
|
|
@@ -1030,7 +1073,10 @@
|
|
|
1030
1073
|
...liveRequests.map((r) => ({ ...r, _kind: "request" })),
|
|
1031
1074
|
];
|
|
1032
1075
|
all.sort((a, b) => new Date(b.created_at || b.timestamp).getTime() - new Date(a.created_at || a.timestamp).getTime());
|
|
1033
|
-
const
|
|
1076
|
+
const filtered = activeEventFilter
|
|
1077
|
+
? all.filter((e) => e._kind === activeEventFilter)
|
|
1078
|
+
: all;
|
|
1079
|
+
const combined = filtered.slice(0, 50);
|
|
1034
1080
|
|
|
1035
1081
|
$("#event-count").textContent = combined.length;
|
|
1036
1082
|
if (!combined.length) {
|
|
@@ -1046,7 +1092,7 @@
|
|
|
1046
1092
|
<span class="method-badge ${escapeHtml(e.method.toLowerCase())}">${escapeHtml(e.method)}</span>
|
|
1047
1093
|
<code>${escapeHtml(e.path)}</code>
|
|
1048
1094
|
<span class="source-badge ${escapeHtml(e.source.toLowerCase())}">${escapeHtml(e.source)}</span>
|
|
1049
|
-
<span style="color: var(--fg-dim); margin-left: auto;">${
|
|
1095
|
+
<span style="color: var(--fg-dim); margin-left: auto;">${relativeTime(e.created_at ?? e.ts ?? e.timestamp)}</span>
|
|
1050
1096
|
</div>
|
|
1051
1097
|
<div class="item-meta">
|
|
1052
1098
|
<span>${escapeHtml(e.actor)}</span>
|
|
@@ -1054,16 +1100,17 @@
|
|
|
1054
1100
|
</div>
|
|
1055
1101
|
`;
|
|
1056
1102
|
}
|
|
1103
|
+
const payload = (() => { try { return e.payload ? JSON.parse(e.payload) : null; } catch { return e.payload || null; } })();
|
|
1057
1104
|
return `
|
|
1058
|
-
<div class="item">
|
|
1105
|
+
<div class="item event-${escapeHtml(e.event_type)}">
|
|
1059
1106
|
<div class="item-row1">
|
|
1060
1107
|
<span class="event-type ${escapeHtml(e.event_type)}">${escapeHtml(e.event_type)}</span>
|
|
1061
1108
|
<code>${escapeHtml(e.entity_type)}/${escapeHtml(e.entity_id)}</code>
|
|
1062
|
-
<span style="color: var(--fg-dim); margin-left: auto;">${
|
|
1109
|
+
<span style="color: var(--fg-dim); margin-left: auto;">${relativeTime(e.created_at ?? e.ts)}</span>
|
|
1063
1110
|
</div>
|
|
1064
1111
|
<div class="item-meta">
|
|
1065
1112
|
${e.actor ? `<span>${escapeHtml(e.actor)}</span>` : ""}
|
|
1066
|
-
${
|
|
1113
|
+
${payload ? `<details><summary style="cursor:pointer;color:var(--fg-faint);font-size:11px;list-style:none;">payload ▸</summary><pre class="event-payload">${escapeHtml(JSON.stringify(payload, null, 2))}</pre></details>` : ""}
|
|
1067
1114
|
</div>
|
|
1068
1115
|
</div>
|
|
1069
1116
|
`;
|
|
@@ -1101,6 +1148,7 @@
|
|
|
1101
1148
|
actions.push(`<button class="btn danger" data-action="delete-spec" data-id="${escapeHtml(spec.id)}">delete</button>`);
|
|
1102
1149
|
setModalBody(`
|
|
1103
1150
|
${actions.length ? `<div class="modal-actions">${actions.join("")}</div>` : ""}
|
|
1151
|
+
<hr class="modal-divider">
|
|
1104
1152
|
${spec.content ? `
|
|
1105
1153
|
<div class="modal-section">
|
|
1106
1154
|
<h3>Spec content <span class="badge">${spec.content.length}B</span></h3>
|
|
@@ -1111,6 +1159,7 @@
|
|
|
1111
1159
|
<h3>Plan v${latest_plan.version} <span class="badge">${latest_plan.content.length}B</span></h3>
|
|
1112
1160
|
<div class="markdown">${renderMarkdown(latest_plan.content)}</div>
|
|
1113
1161
|
</div>` : ""}
|
|
1162
|
+
<hr class="modal-divider">
|
|
1114
1163
|
<div class="modal-section">
|
|
1115
1164
|
<h3>Tasks <span class="badge">${tasks.length}</span></h3>
|
|
1116
1165
|
<div>${tasks.length === 0 ? '<div class="empty">no tasks</div>' : tasks.map((task) => `
|
|
@@ -1161,20 +1210,23 @@
|
|
|
1161
1210
|
if (taskData.spec_id) {
|
|
1162
1211
|
actions.unshift(`<button class="btn" data-action="back-to-spec" data-spec="${escapeHtml(taskData.spec_id)}">← ${escapeHtml(taskData.spec_id)}</button>`);
|
|
1163
1212
|
}
|
|
1213
|
+
const metaRows = [
|
|
1214
|
+
taskData.owner ? `<span class="modal-meta-label">Owner</span><span class="modal-meta-value">${escapeHtml(taskData.owner)}</span>` : "",
|
|
1215
|
+
taskData.taken_at ? `<span class="modal-meta-label">Taken</span><span class="modal-meta-value">${escapeHtml(taskData.taken_at)}</span>` : "",
|
|
1216
|
+
taskData.completed_at ? `<span class="modal-meta-label">Completed</span><span class="modal-meta-value">${escapeHtml(taskData.completed_at)}</span>` : "",
|
|
1217
|
+
taskData.worktree ? `<span class="modal-meta-label">Worktree</span><span class="modal-meta-value"><code>${escapeHtml(taskData.worktree)}</code></span>` : "",
|
|
1218
|
+
taskData.blocked_by?.length ? `<span class="modal-meta-label">Blocked by</span><span class="modal-meta-value">${taskData.blocked_by.map((id) => `<code>${escapeHtml(id)}</code>`).join(", ")}</span>` : "",
|
|
1219
|
+
taskData.packages?.length ? `<span class="modal-meta-label">Packages</span><span class="modal-meta-value">${taskData.packages.map((p) => `<code>${escapeHtml(p)}</code>`).join(", ")}</span>` : "",
|
|
1220
|
+
taskData.quality_gates?.length ? `<span class="modal-meta-label">Quality gates</span><span class="modal-meta-value">${taskData.quality_gates.map((g) => `<code>${escapeHtml(g)}</code>`).join("<br>")}</span>` : "",
|
|
1221
|
+
].filter(Boolean).join("");
|
|
1164
1222
|
setModalBody(`
|
|
1165
1223
|
${actions.length ? `<div class="modal-actions">${actions.join("")}</div>` : ""}
|
|
1224
|
+
<hr class="modal-divider">
|
|
1166
1225
|
<div class="modal-section">
|
|
1167
1226
|
<h3>Metadata</h3>
|
|
1168
|
-
<div class="
|
|
1169
|
-
${taskData.owner ? `<span><strong>owner:</strong> ${escapeHtml(taskData.owner)}</span>` : ""}
|
|
1170
|
-
${taskData.taken_at ? `<span><strong>taken:</strong> ${escapeHtml(taskData.taken_at)}</span>` : ""}
|
|
1171
|
-
${taskData.completed_at ? `<span><strong>completed:</strong> ${escapeHtml(taskData.completed_at)}</span>` : ""}
|
|
1172
|
-
</div>
|
|
1173
|
-
${taskData.worktree ? `<div style="margin-top:8px;"><strong>worktree:</strong> <code>${escapeHtml(taskData.worktree)}</code></div>` : ""}
|
|
1174
|
-
${taskData.blocked_by?.length ? `<div style="margin-top:8px;"><strong>blocked by:</strong> ${taskData.blocked_by.map((id) => `<code>${escapeHtml(id)}</code>`).join(", ")}</div>` : ""}
|
|
1175
|
-
${taskData.packages?.length ? `<div style="margin-top:8px;"><strong>packages:</strong> ${taskData.packages.map((p) => `<code>${escapeHtml(p)}</code>`).join(", ")}</div>` : ""}
|
|
1176
|
-
${taskData.quality_gates?.length ? `<div style="margin-top:8px;"><strong>quality gates:</strong><ul style="margin: 4px 0 0 16px;">${taskData.quality_gates.map((g) => `<li><code>${escapeHtml(g)}</code></li>`).join("")}</ul></div>` : ""}
|
|
1227
|
+
<div class="modal-meta-grid">${metaRows}</div>
|
|
1177
1228
|
</div>
|
|
1229
|
+
<hr class="modal-divider">
|
|
1178
1230
|
${taskData.description ? `
|
|
1179
1231
|
<div class="modal-section">
|
|
1180
1232
|
<h3>Description / workplan</h3>
|
|
@@ -1495,13 +1547,29 @@
|
|
|
1495
1547
|
worktreeElapsedTimer = setInterval(() => {
|
|
1496
1548
|
document.querySelectorAll('[data-elapsed]').forEach((el) => {
|
|
1497
1549
|
const iso = el.getAttribute('data-elapsed');
|
|
1498
|
-
|
|
1550
|
+
if (iso) {
|
|
1551
|
+
el.textContent = elapsedSince(iso);
|
|
1552
|
+
const minutes = Math.floor((Date.now() - new Date(iso).getTime()) / 60000);
|
|
1553
|
+
if (minutes >= 60) el.style.color = 'var(--red)';
|
|
1554
|
+
else if (minutes >= 30) el.style.color = 'var(--yellow)';
|
|
1555
|
+
else el.style.color = 'var(--fg-dim)';
|
|
1556
|
+
} else {
|
|
1557
|
+
el.textContent = '';
|
|
1558
|
+
}
|
|
1499
1559
|
});
|
|
1500
1560
|
}, 1000);
|
|
1501
1561
|
// Immediate first update
|
|
1502
1562
|
document.querySelectorAll('[data-elapsed]').forEach((el) => {
|
|
1503
1563
|
const iso = el.getAttribute('data-elapsed');
|
|
1504
|
-
|
|
1564
|
+
if (iso) {
|
|
1565
|
+
el.textContent = elapsedSince(iso);
|
|
1566
|
+
const minutes = Math.floor((Date.now() - new Date(iso).getTime()) / 60000);
|
|
1567
|
+
if (minutes >= 60) el.style.color = 'var(--red)';
|
|
1568
|
+
else if (minutes >= 30) el.style.color = 'var(--yellow)';
|
|
1569
|
+
else el.style.color = 'var(--fg-dim)';
|
|
1570
|
+
} else {
|
|
1571
|
+
el.textContent = '';
|
|
1572
|
+
}
|
|
1505
1573
|
});
|
|
1506
1574
|
}
|
|
1507
1575
|
}
|
|
@@ -1525,6 +1593,14 @@
|
|
|
1525
1593
|
}
|
|
1526
1594
|
}
|
|
1527
1595
|
|
|
1596
|
+
function gitFileColor(line) {
|
|
1597
|
+
if (/^(\?\?|!!)\s/.test(line)) return 'var(--fg-faint)';
|
|
1598
|
+
if (/^A[\s]/.test(line) || /^[A-Z]A/.test(line)) return 'var(--green)';
|
|
1599
|
+
if (/^D[\s]/.test(line) || /^[A-Z]D/.test(line)) return 'var(--red)';
|
|
1600
|
+
if (/M/.test(line.slice(0,2))) return 'var(--yellow)';
|
|
1601
|
+
return 'var(--fg-dim)';
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1528
1604
|
function updateWorktreeCard(id, data) {
|
|
1529
1605
|
const badgeEl = $(`[data-state-badge="${CSS.escape(id)}"]`);
|
|
1530
1606
|
const filesEl = $(`[data-files="${CSS.escape(id)}"]`);
|
|
@@ -1534,6 +1610,8 @@
|
|
|
1534
1610
|
const state = worktreeStateBadge(data);
|
|
1535
1611
|
badgeEl.className = `worktree-state ${state.cls}`;
|
|
1536
1612
|
badgeEl.textContent = state.text;
|
|
1613
|
+
const card = badgeEl.closest('.worktree-card');
|
|
1614
|
+
if (card) card.dataset.cardState = state.cls;
|
|
1537
1615
|
const agent = agentProcesses.find((a) => a.taskId === id);
|
|
1538
1616
|
if (processEl) {
|
|
1539
1617
|
const ownerEl = badgeEl.closest('.worktree-card')?.querySelector('.owner');
|
|
@@ -1548,7 +1626,7 @@
|
|
|
1548
1626
|
}
|
|
1549
1627
|
if (filesEl) {
|
|
1550
1628
|
filesEl.innerHTML = data.files?.length
|
|
1551
|
-
? data.files.map((f) => `<li>${escapeHtml(f)}</li>`).join("")
|
|
1629
|
+
? data.files.map((f) => `<li style="color: ${gitFileColor(f)}">${escapeHtml(f)}</li>`).join("")
|
|
1552
1630
|
: "";
|
|
1553
1631
|
}
|
|
1554
1632
|
if (commitEl) {
|
|
@@ -1572,6 +1650,7 @@
|
|
|
1572
1650
|
renderSpecs(specs);
|
|
1573
1651
|
renderTasks(tasks);
|
|
1574
1652
|
renderWorktreeCards(tasks);
|
|
1653
|
+
lastDbEvents = dbEvents;
|
|
1575
1654
|
renderCombinedEvents(dbEvents);
|
|
1576
1655
|
} catch (err) {
|
|
1577
1656
|
console.error("refresh failed:", err);
|
|
@@ -1652,7 +1731,9 @@
|
|
|
1652
1731
|
}
|
|
1653
1732
|
|
|
1654
1733
|
// -------- Boot --------
|
|
1655
|
-
$(
|
|
1734
|
+
$('#modal-bg').addEventListener('click', (e) => {
|
|
1735
|
+
if (e.target === e.currentTarget) closeModal();
|
|
1736
|
+
});
|
|
1656
1737
|
$("#modal-close").addEventListener("click", closeModal);
|
|
1657
1738
|
document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeModal(); });
|
|
1658
1739
|
$$("#task-filters .filter-btn").forEach((btn) => {
|
|
@@ -1687,6 +1768,14 @@
|
|
|
1687
1768
|
refresh();
|
|
1688
1769
|
});
|
|
1689
1770
|
});
|
|
1771
|
+
$$("#event-filters .filter-btn").forEach((btn) => {
|
|
1772
|
+
btn.addEventListener("click", () => {
|
|
1773
|
+
activeEventFilter = btn.dataset.eventFilter;
|
|
1774
|
+
$$("#event-filters .filter-btn").forEach((b) => b.classList.remove("active"));
|
|
1775
|
+
btn.classList.add("active");
|
|
1776
|
+
renderCombinedEvents(lastDbEvents);
|
|
1777
|
+
});
|
|
1778
|
+
});
|
|
1690
1779
|
$("#new-spec-btn").addEventListener("click", openNewSpecModal);
|
|
1691
1780
|
$("#new-task-btn").addEventListener("click", openNewTaskModal);
|
|
1692
1781
|
|