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 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 five Claude Code skills covering the full development loop. They activate automatically when `.feinai/feinai.db` is present:
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-sdd` | Teaches Claude the feinai workflow and concepts |
119
- | `feinai-write-spec` | Writes spec + plan into feinai from a design thread |
120
- | `feinai-write-tasks`| Decomposes a plan into atomic tasks with parallelism |
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-sdd feinai-write-spec feinai-write-tasks feinai-dispatch feinai-implement; do
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,6 +1,6 @@
1
1
  {
2
2
  "name": "feinai",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
4
4
  "description": "Task & spec manager for AI agents — parallel worktrees, live dashboard, SDD skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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`. Writes a complete spec + plan into feinai in a single session. Replaces `brainstorming` + `writing-plans` from superpowers when feinai is active. Reads project context (CLAUDE.md, ARCHITECTURE.md, README.md), asks clarifying questions only when context has gaps, and produces spec+plan atomically with one `feinai spec add` + one `feinai plan add`.
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
- Write a spec + plan for a feature in one session. Output goes to feinai, not markdown files.
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
- ## The flow — three entry modes
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 1 — Load project context
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 2 — Draft and write the spec
66
+ ## Phase 3 — Draft and write the spec + plan (full pipeline only)
54
67
 
55
- **Spec answers: what and why.** Not how.
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 are written by `feinai-write-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 — Hand off
138
+ ## Phase 4 — Build the task graph
131
139
 
132
- Tell the user:
133
- > Spec + plan written: SPEC-NNN. View with `feinai spec show SPEC-NNN --full`.
134
- > Next step: run `/feinai-write-tasks SPEC-NNN` to break it down into executable tasks.
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
- Do NOT create tasks here. That's `feinai-write-tasks`. One responsibility per skill.
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
- ## Key questions to ask (during, only if needed)
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
- These come up mid-session. Ask them inline, one at a time, brief:
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 the spec, then they review with `feinai spec show`
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 the spec drafting to a subagent (e.g. for parallel research),
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
@@ -48,7 +48,7 @@ import {
48
48
  type OutputFormat,
49
49
  } from "./format";
50
50
 
51
- const VERSION = "0.6.4";
51
+ const VERSION = "0.6.7";
52
52
 
53
53
  interface ParsedArgs {
54
54
  positional: string[];
@@ -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.style.cssText = "position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:9999;display:flex;align-items:center;justify-content:center;";
832
+ overlay.className = "confirm-overlay";
790
833
  overlay.innerHTML = `
791
- <div style="background:var(--bg-card);border:1px solid var(--border-strong);border-radius:10px;padding:24px;min-width:280px;max-width:380px;">
792
- <p style="margin:0 0 16px;font-size:15px;">${escapeHtml(message)}</p>
793
- <label style="display:flex;align-items:center;gap:8px;font-size:12px;color:var(--fg-dim);margin-bottom:16px;cursor:pointer;">
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 style="display:flex;gap:8px;justify-content:flex-end;">
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 combined = all.slice(0, 50);
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;">${escapeHtml(e.timestamp)}</span>
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;">${escapeHtml(e.created_at)}</span>
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
- ${e.payload ? `<code style="color: var(--fg-faint);">${escapeHtml(e.payload)}</code>` : ""}
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="item-meta">
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
- el.textContent = iso ? elapsedSince(iso) : '';
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
- el.textContent = iso ? elapsedSince(iso) : '';
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
- $("#modal-bg").addEventListener("click", (e) => { if (e.target === $("#modal-bg")) closeModal(); });
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