feinai 0.6.5 → 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/CHANGELOG.md CHANGED
@@ -12,23 +12,23 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
12
12
  ## [0.5.0] - 2026-06-10
13
13
 
14
14
  ### Added
15
- - `tasca git <cmd>` — safe git wrapper subcommand. Passes through to `opengit`, enforcing a worktree-only whitelist (blocks branch, merge, rebase, checkout, etc.)
16
- - `opengit` shipped as a binary alongside `tasca` — `bun install -g tasca` now installs both
17
- - `tasca unblock <TASK-ID> --dep <TASK-ID>` — remove a specific dependency from a task
18
- - `tasca edit --clear-blocked-by` — clear all dependencies from a task
19
- - Live Agents Monitor: working directory, repo name, and `.tasca` path shown per agent card
15
+ - `feinai git <cmd>` — safe git wrapper subcommand. Passes through to `opengit`, enforcing a worktree-only whitelist (blocks branch, merge, rebase, checkout, etc.)
16
+ - `opengit` shipped as a binary alongside `feinai` — `bun install -g feinai` now installs both
17
+ - `feinai unblock <TASK-ID> --dep <TASK-ID>` — remove a specific dependency from a task
18
+ - `feinai edit --clear-blocked-by` — clear all dependencies from a task
19
+ - Live Agents Monitor: working directory, repo name, and `.feinai` path shown per agent card
20
20
  - Live Agents Monitor: presence indicator — gray dot when idle, green dot with ripple animation when agents are active
21
21
  - Live Agents Monitor: elapsed time and spec ID per card
22
- - `tasca init` now auto-adds `.tasca/` to `.gitignore` if in a git repo
23
- - Skills: `tasca-implement` — claim one pending task, implement in isolated worktree, run quality gates, push to main
22
+ - `feinai init` now auto-adds `.feinai/` to `.gitignore` if in a git repo
23
+ - Skills: `feinai-implement` — claim one pending task, implement in isolated worktree, run quality gates, push to main
24
24
 
25
25
  ### Changed
26
26
  - Dashboard is now English-only — removed i18n/language selector
27
- - `tasca-implement` SKILL.md updated to use `tasca git` instead of `opengit` directly
27
+ - `feinai-implement` SKILL.md updated to use `feinai git` instead of `opengit` directly
28
28
  - `package.json`: added `repository`, `homepage`, `bugs` fields
29
29
 
30
30
  ### Skills included
31
- `tasca-sdd` · `tasca-write-spec` · `tasca-write-tasks` · `tasca-dispatch` · `tasca-implement`
31
+ `feinai-sdd` · `feinai-write-spec` · `feinai-write-tasks` · `feinai-dispatch` · `feinai-implement`
32
32
 
33
33
  ---
34
34
 
@@ -37,11 +37,11 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
37
37
  Initial public release. Core CLI with specs, plans, tasks, HTTP dashboard, and SDD skills.
38
38
 
39
39
  ### Features
40
- - `tasca init / status / list / add / show / take / done / fail / block / release / reopen / edit`
41
- - `tasca spec` — spec lifecycle (add, start, done, archive, set-content, edit)
42
- - `tasca plan` — plan revisions per spec
43
- - `tasca server` — HTTP dashboard + REST API + SSE live updates
40
+ - `feinai init / status / list / add / show / take / done / fail / block / release / reopen / edit`
41
+ - `feinai spec` — spec lifecycle (add, start, done, archive, set-content, edit)
42
+ - `feinai plan` — plan revisions per spec
43
+ - `feinai server` — HTTP dashboard + REST API + SSE live updates
44
44
  - Live Agents Monitor — real-time view of in-progress tasks with worktree state
45
45
  - Atomic `take` — SQL-level concurrency safety for parallel agents
46
46
  - Append-only events audit log
47
- - Skills: `tasca-sdd`, `tasca-write-spec`, `tasca-write-tasks`, `tasca-dispatch`
47
+ - Skills: `feinai-sdd`, `feinai-write-spec`, `feinai-write-tasks`, `feinai-dispatch`
package/README.md CHANGED
@@ -73,7 +73,7 @@ State lives in a local SQLite file. No cloud, no external service, no account.
73
73
  - Installs two binaries:
74
74
  - `feinai` – main CLI + embedded HTTP server and dashboard.
75
75
  - `opengit` – safe git wrapper for parallel worktrees.
76
- - Stores all state in a single SQLite file: `.tasca/tasca.db`, discovered by walking up from the current directory (like `.git`).
76
+ - Stores all state in a single SQLite file: `.feinai/feinai.db`, discovered by walking up from the current directory (like `.git`).
77
77
 
78
78
  No background services are required beyond the optional dashboard server.
79
79
 
@@ -83,7 +83,7 @@ No background services are required beyond the optional dashboard server.
83
83
 
84
84
  feinai is designed to be "boring" infrastructure:
85
85
 
86
- - **Local‑only state.** All coordination lives in `.tasca/tasca.db` in your repo. There is no remote backend.
86
+ - **Local‑only state.** All coordination lives in `.feinai/feinai.db` in your repo. There is no remote backend.
87
87
  - **Explicit operations.** Every change to specs, plans, tasks and worktrees goes through the CLI or HTTP API and is logged in `events`.
88
88
  - **Safe git workflow.** `feinai git` wraps git and blocks operations that can corrupt parallel worktrees (branch, checkout, merge, rebase, reset, fetch, pull, clone).
89
89
  - **Auditability.** Every task has a clear chain: spec → plan → task → worktree → events. Every agent identity can be traced (`$FEINAI_USER` override supported).
@@ -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 `.tasca/tasca.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
  ```
@@ -169,7 +167,7 @@ sudo ln -sf ~/.bun/bin/bun /usr/local/bin/bun
169
167
  cd my-project
170
168
 
171
169
  # Initialize local state
172
- feinai init # creates .tasca/tasca.db, adds to .gitignore
170
+ feinai init # creates .feinai/feinai.db, adds to .gitignore
173
171
 
174
172
  # Add a spec and plan
175
173
  feinai spec add SPEC-001 "User authentication" --content "..."
@@ -197,7 +195,7 @@ feinai server # live dashboard at http://127.0.0.1:8272
197
195
  High‑level CLI:
198
196
 
199
197
  ```text
200
- feinai init Create .tasca/tasca.db
198
+ feinai init Create .feinai/feinai.db
201
199
  feinai status Show counts: pending / in_progress / completed
202
200
  feinai list [--pending] [--spec X] List tasks
203
201
  feinai add ID "subject" Create task
@@ -267,7 +265,7 @@ Use your normal git tooling inside each worktree; use `feinai git` when operatin
267
265
  ## Architecture
268
266
 
269
267
  ```text
270
- <project>/.tasca/tasca.db # local SQLite database (discovered like .git)
268
+ <project>/.feinai/feinai.db # local SQLite database (discovered like .git)
271
269
 
272
270
  specs — what to build
273
271
  plans — how to build it (versioned)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feinai",
3
- "version": "0.6.5",
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": {
@@ -66,7 +66,7 @@ PATH issue in non-interactive SSH session. `~/.bun/bin` is not loaded. Tell the
66
66
 
67
67
  ---
68
68
 
69
- ### Failure: `feinai status` exits with code 2 (`no .tasca/tasca.db found`)
69
+ ### Failure: `feinai status` exits with code 2 (`no .feinai/feinai.db found`)
70
70
 
71
71
  feinai is installed but no DB in this project. Ask the user:
72
72
 
@@ -206,7 +206,7 @@ Spawn a subagent with this prompt template:
206
206
 
207
207
  ### Step E — Wait for results
208
208
 
209
- Each subagent returns `done` or `fail`. The tasca DB is the source of truth — re-query it:
209
+ Each subagent returns `done` or `fail`. The feinai DB is the source of truth — re-query it:
210
210
 
211
211
  ```bash
212
212
  feinai show TASK-X --json
@@ -1,17 +1,17 @@
1
1
  ---
2
2
  name: feinai-implement
3
- description: Use when a feinai task needs to be executed. Claims one pending task from tasca, implements it in an isolated worktree, runs quality gates, and pushes to main. Designed to be dispatched by feinai-dispatch or run standalone for a single task.
3
+ description: Use when a feinai task needs to be executed. Claims one pending task from feinai, implements it in an isolated worktree, runs quality gates, and pushes to main. Designed to be dispatched by feinai-dispatch or run standalone for a single task.
4
4
  ---
5
5
 
6
6
  # feinai-implement
7
7
 
8
- Claim one pending task from tasca, implement it in an isolated worktree, run quality gates, push to main.
8
+ Claim one pending task from feinai, implement it in an isolated worktree, run quality gates, push to main.
9
9
 
10
10
  ## Preconditions
11
11
 
12
12
  1. `feinai status` succeeds and there is at least one pending task
13
13
  2. Current working directory is a clean git repo
14
- 3. `feinai git status` works (feinai git is bundled with tasca — no separate setup needed)
14
+ 3. `feinai git status` works (feinai git is bundled with feinai — no separate setup needed)
15
15
 
16
16
  If any fails: stop and report. Do not improvise.
17
17
 
@@ -79,7 +79,7 @@ Gates fallan → seguí "Si algo falla".
79
79
  **Done = 3 hechos observables:**
80
80
  1. Quality gates pasan sin errores
81
81
  2. Los archivos de la tarea existen con contenido correcto
82
- 3. Commit limpio en `main` y tarea en estado `completed` en tasca
82
+ 3. Commit limpio en `main` y tarea en estado `completed` en feinai
83
83
 
84
84
  ---
85
85
 
@@ -102,7 +102,7 @@ Gates fallan, push falla, o error en cualquier paso:
102
102
 
103
103
  ## Git — `feinai git` exclusivamente
104
104
 
105
- `git` y `gh` están bloqueados. Usá `feinai git` para todo — es opengit bundleado con tasca.
105
+ `git` y `gh` están bloqueados. Usá `feinai git` para todo — es opengit bundleado con feinai.
106
106
 
107
107
  **Permitido:**
108
108
  - `feinai git worktree add/list/lock/unlock`
@@ -125,7 +125,7 @@ Si `feinai git` falla → **STOP**. No reintentes, no uses `git`. Reportá al us
125
125
  **No modifiques:**
126
126
  - `AGENTS.md`, `CLAUDE.md`
127
127
  - Archivos de configuración de CI/CD, infra, o secretos (`.env`, `.env.*`)
128
- - La DB de tasca directamente
128
+ - La DB de feinai directamente
129
129
 
130
130
  **Código:**
131
131
  - Sin `any` sin comentario justificado en la misma línea
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: feinai-sdd
3
- description: Use when working in spec-driven development (SDD) workflows that involve brainstorming, writing-plans, or subagent-driven-development AND the project has a `\.feinai/feinai.db` file. Replaces creating markdown files in `docs/superpowers/specs/` and `docs/superpowers/plans/` with atomic `feinai` CLI calls, keeping state queryable, race-free, and audit-logged. Also use when the user asks to create a spec, add a task, claim work, or mark progress, in a project that uses tasca.
3
+ description: Use when working in spec-driven development (SDD) workflows that involve brainstorming, writing-plans, or subagent-driven-development AND the project has a `\.feinai/feinai.db` file. Replaces creating markdown files in `docs/superpowers/specs/` and `docs/superpowers/plans/` with atomic `feinai` CLI calls, keeping state queryable, race-free, and audit-logged. Also use when the user asks to create a spec, add a task, claim work, or mark progress.
4
4
  ---
5
5
 
6
6
  # feinai SDD integration
@@ -21,7 +21,7 @@ Invoke when **all** of the following are true:
21
21
 
22
22
  1. The project has a `\.feinai/feinai.db` file (walk up the directory tree from
23
23
  the current working directory; feinai uses the same discovery pattern as git).
24
- Check with: `feinai status` (exit code 0 = tasca is set up).
24
+ Check with: `feinai status` (exit code 0 = feinai is set up).
25
25
  2. You are about to:
26
26
  - Write a spec via the `brainstorming` skill, OR
27
27
  - Write a plan via the `writing-plans` skill, OR
@@ -274,7 +274,7 @@ field will read `bun:<pid>:<user>` for everyone.
274
274
 
275
275
  | Need | Command |
276
276
  |---|---|
277
- | Is tasca set up here? | `feinai status` |
277
+ | Is feinai set up here? | `feinai status` |
278
278
  | Next pending task | `feinai list --pending --json` |
279
279
  | Claim a task atomically | `feinai take TASK-X` |
280
280
  | Read a spec | `feinai spec content SPEC-X` |
@@ -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 `.tasca/tasca.db`. Writes a complete spec + plan into tasca 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 tasca, 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
- Run `feinai status` (exit 0 = tasca is active). If not active, stop and ask the
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 tasca 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 tasca 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,11 +1,11 @@
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
7
7
 
8
- Read an existing spec + plan from tasca, produce the executable task set.
8
+ Read an existing spec + plan from feinai, produce the executable task set.
9
9
 
10
10
  ## Preconditions
11
11
 
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[];
@@ -381,9 +381,9 @@ async function cmdDestroy(args: ParsedArgs): Promise<void> {
381
381
  }
382
382
  }
383
383
 
384
- const tascaDir = dbPath.replace(/\/tasca\.db$/, "");
385
- rmSync(tascaDir, { recursive: true, force: true });
386
- console.log(`Destroyed ${tascaDir}`);
384
+ const feinaiDir = dbPath.replace(/\/feinai\.db$/, "");
385
+ rmSync(feinaiDir, { recursive: true, force: true });
386
+ console.log(`Destroyed ${feinaiDir}`);
387
387
  }
388
388
 
389
389
  function cmdInit(args: ParsedArgs): void {
@@ -770,7 +770,7 @@ function cmdSpec(rest: string[], args: ParsedArgs, format: OutputFormat): void {
770
770
  async function cmdServer(args: ParsedArgs): Promise<void> {
771
771
  const port = Number(args.options.port ?? "8272");
772
772
 
773
- // --down: kill whatever is listening on the tasca port
773
+ // --down: kill whatever is listening on the feinai port
774
774
  if (args.flags["down"]) {
775
775
  if (Number.isNaN(port) || port < 1 || port > 65535) {
776
776
  console.error("Error: --port must be a valid port number");
@@ -785,7 +785,7 @@ async function cmdServer(args: ParsedArgs): Promise<void> {
785
785
  for (const pid of pids) {
786
786
  try {
787
787
  process.kill(Number(pid), "SIGTERM");
788
- console.log(`Stopped tasca server (PID ${pid}).`);
788
+ console.log(`Stopped feinai server (PID ${pid}).`);
789
789
  } catch {
790
790
  console.error(`Failed to kill PID ${pid}.`);
791
791
  }
@@ -812,8 +812,8 @@ async function cmdServer(args: ParsedArgs): Promise<void> {
812
812
  : [process.execPath, ...process.argv.slice(1).filter(noDaemon)];
813
813
  const child = Bun.spawn(childArgs, { detached: true, stdio: ["ignore", "ignore", "ignore"] });
814
814
  child.unref();
815
- console.log(`tasca dashboard → http://${host}:${port}`);
816
- console.log(`Stop with: tasca server --down`);
815
+ console.log(`feinai dashboard → http://${host}:${port}`);
816
+ console.log(`Stop with: feinai server --down`);
817
817
  return;
818
818
  }
819
819
 
@@ -821,8 +821,8 @@ async function cmdServer(args: ParsedArgs): Promise<void> {
821
821
  const { startServer } = await import("./server");
822
822
  const server = startServer({ port, host });
823
823
 
824
- console.log(`tasca dashboard listening at ${server.url}`);
825
- console.log(`Stop with: tasca server --down`);
824
+ console.log(`feinai dashboard listening at ${server.url}`);
825
+ console.log(`Stop with: feinai server --down`);
826
826
 
827
827
  // Keep process alive until SIGINT
828
828
  process.on("SIGINT", () => {
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>tasca dashboard</title>
6
+ <title>feinai dashboard</title>
7
7
  <style>
8
8
  :root {
9
9
  --bg: #0f1117;
@@ -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,13 +685,22 @@
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>
672
700
  <header>
673
701
  <h1>
674
702
  <span class="live-indicator" id="live"></span>
675
- tasca
703
+ feinai
676
704
  </h1>
677
705
  <div class="stats" id="stats"></div>
678
706
  <div class="header-actions">
@@ -719,11 +747,16 @@
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>
725
758
 
726
- <footer>tasca · streaming live · click any item for details &amp; actions · <span id="dash-version">v—</span></footer>
759
+ <footer>feinai · streaming live · click any item for details &amp; actions · <span id="dash-version">v—</span></footer>
727
760
 
728
761
  <div class="modal-bg" id="modal-bg">
729
762
  <div class="modal" id="modal">
@@ -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 --------
@@ -784,23 +827,23 @@
784
827
  // -------- Confirm modal --------
785
828
  function confirm(message, skipKey) {
786
829
  return new Promise((resolve) => {
787
- if (localStorage.getItem("tasca-skip-" + skipKey) === "1") { resolve(true); return; }
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>
800
843
  </div>`;
801
844
  document.body.appendChild(overlay);
802
845
  overlay.querySelector("#confirm-ok").onclick = () => {
803
- if (overlay.querySelector("#skip-cb").checked) localStorage.setItem("tasca-skip-" + skipKey, "1");
846
+ if (overlay.querySelector("#skip-cb").checked) localStorage.setItem("feinai-skip-" + skipKey, "1");
804
847
  document.body.removeChild(overlay); resolve(true);
805
848
  };
806
849
  overlay.querySelector("#confirm-cancel").onclick = () => { document.body.removeChild(overlay); resolve(false); };
@@ -827,7 +870,7 @@
827
870
  async function api(method, path, body) {
828
871
  const res = await fetch(path, {
829
872
  method,
830
- headers: body ? { "Content-Type": "application/json", "X-Tasca-Actor": "dashboard" } : { "X-Tasca-Actor": "dashboard" },
873
+ headers: body ? { "Content-Type": "application/json", "X-Feinai-Actor": "dashboard" } : { "X-Feinai-Actor": "dashboard" },
831
874
  body: body ? JSON.stringify(body) : undefined,
832
875
  });
833
876
  if (!res.ok) {
@@ -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>
@@ -1456,7 +1508,7 @@
1456
1508
  : '';
1457
1509
  const workingDir = task.worktree || appContext.repoRoot || "—";
1458
1510
  const repoName = appContext.repoName || "—";
1459
- const tascaLoc = appContext.tascaPath || "—";
1511
+ const feinaiLoc = appContext.feinaiPath || "—";
1460
1512
  const card = document.createElement('div');
1461
1513
  card.className = 'worktree-card';
1462
1514
  card.setAttribute('data-worktree-task', task.id);
@@ -1469,7 +1521,7 @@
1469
1521
  <div class="worktree-context">
1470
1522
  <div class="context-row"><span class="context-label">Working directory</span><span class="context-value">${escapeHtml(workingDir)}</span></div>
1471
1523
  <div class="context-row"><span class="context-label">Repo</span><span class="context-value">${escapeHtml(repoName)}</span></div>
1472
- <div class="context-row"><span class="context-label">.feinai</span><span class="context-value">${escapeHtml(tascaLoc)}</span></div>
1524
+ <div class="context-row"><span class="context-label">.feinai</span><span class="context-value">${escapeHtml(feinaiLoc)}</span></div>
1473
1525
  </div>
1474
1526
  <span class="elapsed-time" data-elapsed="${escapeHtml(task.taken_at ?? '')}"></span>
1475
1527
  </div>
@@ -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
 
package/src/db.ts CHANGED
@@ -1,47 +1,10 @@
1
- import { existsSync, mkdirSync, renameSync } from "node:fs";
1
+ import { existsSync, mkdirSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { openSqlite, type DbAdapter } from "./sqlite-adapter";
4
4
 
5
5
  const DB_DIR = ".feinai";
6
6
  const DB_FILE = "feinai.db";
7
7
 
8
- // v0.5.x migration: rename .tasca/tasca.db → .feinai/feinai.db
9
- // TODO: remove in v0.7
10
- function migrateLegacyDir(dir: string): void {
11
- const legacyDir = join(dir, ".tasca");
12
- const legacyDb = join(legacyDir, "tasca.db");
13
- const newDir = join(dir, ".feinai");
14
- const newDb = join(newDir, "feinai.db");
15
- if (!existsSync(legacyDb) || existsSync(newDb)) return;
16
-
17
- // Prompt user — only works in TTY contexts
18
- if (process.stdout.isTTY) {
19
- process.stdout.write(
20
- `\n⚠ Found legacy .tasca/tasca.db at ${legacyDir}\n` +
21
- ` Rename to .feinai/feinai.db? [Y/n] `
22
- );
23
- const buf = Buffer.alloc(4);
24
- let answer = "y";
25
- try {
26
- const n = require("node:fs").readSync(0, buf, 0, 4, null);
27
- answer = buf.slice(0, n).toString().trim().toLowerCase() || "y";
28
- } catch {}
29
- if (answer !== "y" && answer !== "") {
30
- process.stdout.write("Skipped. Run 'feinai init' to create a new DB.\n\n");
31
- return;
32
- }
33
- } else {
34
- // Non-interactive (agent/CI): migrate silently
35
- }
36
-
37
- if (!existsSync(newDir)) mkdirSync(newDir, { recursive: true });
38
- renameSync(legacyDb, newDb);
39
- try { require("node:fs").rmdirSync(legacyDir); } catch {}
40
- if (process.stdout.isTTY) {
41
- process.stdout.write(`✓ Migrated to .feinai/feinai.db\n\n`);
42
- }
43
- }
44
-
45
8
  export type DbInstance = DbAdapter;
46
9
 
47
10
  /**
@@ -189,8 +152,6 @@ function applySchema(db: DbInstance): void {
189
152
  export function findDbPath(startDir: string = process.cwd()): string | null {
190
153
  let current = resolve(startDir);
191
154
  while (true) {
192
- // migrate legacy .tasca/tasca.db → .feinai/feinai.db if found
193
- migrateLegacyDir(current);
194
155
  const candidate = join(current, DB_DIR, DB_FILE);
195
156
  if (existsSync(candidate)) return candidate;
196
157
  const parent = dirname(current);
@@ -204,11 +165,11 @@ export function findDbPath(startDir: string = process.cwd()): string | null {
204
165
  * Returns the absolute path of the created DB file.
205
166
  */
206
167
  export function initDb(dir: string = process.cwd()): string {
207
- const tascaDir = join(dir, DB_DIR);
208
- const dbPath = join(tascaDir, DB_FILE);
168
+ const feinaiDir = join(dir, DB_DIR);
169
+ const dbPath = join(feinaiDir, DB_FILE);
209
170
 
210
- if (!existsSync(tascaDir)) {
211
- mkdirSync(tascaDir, { recursive: true });
171
+ if (!existsSync(feinaiDir)) {
172
+ mkdirSync(feinaiDir, { recursive: true });
212
173
  }
213
174
 
214
175
  const db = openSqlite(dbPath, { create: true });
package/src/format.ts CHANGED
@@ -96,7 +96,7 @@ export function formatSpec(
96
96
  if (opts.includeContent) {
97
97
  for (const line of spec.content.split("\n")) lines.push(` ${line}`);
98
98
  } else {
99
- lines.push(` ${c(format, "dim", "(use --full to view; tasca spec content " + spec.id + " to export)")}`);
99
+ lines.push(` ${c(format, "dim", "(use --full to view; feinai spec content " + spec.id + " to export)")}`);
100
100
  }
101
101
  }
102
102
  if (opts.plan) {
@@ -104,7 +104,7 @@ export function formatSpec(
104
104
  if (opts.includeContent) {
105
105
  for (const line of opts.plan.content.split("\n")) lines.push(` ${line}`);
106
106
  } else {
107
- lines.push(` ${c(format, "dim", "(use --full to view; tasca plan show " + spec.id + " to export)")}`);
107
+ lines.push(` ${c(format, "dim", "(use --full to view; feinai plan show " + spec.id + " to export)")}`);
108
108
  }
109
109
  }
110
110
  return lines.join("\n");
@@ -154,7 +154,7 @@ export function formatStatus(
154
154
  if (format === "json") return JSON.stringify(stats, null, 2);
155
155
  const serverLine = stats.serverRunning
156
156
  ? c(format, "green", `server: running → http://127.0.0.1:${stats.serverPort}`)
157
- : c(format, "dim", `server: stopped (tasca server -d to start)`);
157
+ : c(format, "dim", `server: stopped (feinai server -d to start)`);
158
158
  return [
159
159
  `${c(format, "yellow", `pending: ${stats.pending}`)}`,
160
160
  `${c(format, "blue", `in_progress: ${stats.in_progress}`)}`,
package/src/server.ts CHANGED
@@ -250,7 +250,7 @@ function serverError(err: unknown): Response {
250
250
 
251
251
  /**
252
252
  * Determine actor for an inbound HTTP mutation. Priority:
253
- * 1. X-Tasca-Actor header (explicit)
253
+ * 1. X-Feinai-Actor header (explicit)
254
254
  * 2. derived from request: "dashboard:<host>" or "api:<user-agent>"
255
255
  */
256
256
  function actorFromRequest(req: Request): string {
@@ -258,7 +258,7 @@ function actorFromRequest(req: Request): string {
258
258
  // iterate manually to be safe.
259
259
  let explicit: string | undefined;
260
260
  for (const [k, v] of req.headers.entries()) {
261
- if (k.toLowerCase() === "x-tasca-actor") {
261
+ if (k.toLowerCase() === "x-feinai-actor") {
262
262
  explicit = v;
263
263
  break;
264
264
  }
@@ -294,9 +294,9 @@ async function resolveDashboardVersion(): Promise<string> {
294
294
  const root = findRepoRoot();
295
295
  if (!root) return "";
296
296
  try {
297
- const countResult = await Bun.$`git log --oneline -- tools/tasca/src/dashboard.html | wc -l`.cwd(root).text();
297
+ const countResult = await Bun.$`git log --oneline -- src/dashboard.html | wc -l`.cwd(root).text();
298
298
  const count = parseInt(countResult.trim(), 10);
299
- const hashResult = await Bun.$`git log -1 --format=%h -- tools/tasca/src/dashboard.html`.cwd(root).text();
299
+ const hashResult = await Bun.$`git log -1 --format=%h -- src/dashboard.html`.cwd(root).text();
300
300
  const hash = hashResult.trim();
301
301
  cachedDashboardVersion = `dashboard-v${count} (${hash})`;
302
302
  } catch {
@@ -325,7 +325,7 @@ export function startServer(opts: ServerOptions): { url: string; stop: () => voi
325
325
  headers: {
326
326
  "Access-Control-Allow-Origin": "*",
327
327
  "Access-Control-Allow-Methods": "GET, POST, DELETE, PATCH, OPTIONS",
328
- "Access-Control-Allow-Headers": "Content-Type, X-Tasca-Actor",
328
+ "Access-Control-Allow-Headers": "Content-Type, X-Feinai-Actor",
329
329
  },
330
330
  });
331
331
  }
@@ -441,18 +441,18 @@ export function startServer(opts: ServerOptions): { url: string; stop: () => voi
441
441
  return json(agents);
442
442
  }
443
443
 
444
- // GET /api/context — repo and tasca DB context info for dashboard
444
+ // GET /api/context — repo and feinai DB context info for dashboard
445
445
  if (path === "/api/context" && method === "GET") {
446
446
  const repoRoot = findRepoRoot();
447
447
  const dbPath = findDbPath();
448
448
  const home = homedir();
449
- const tascaPath = dbPath?.startsWith(home)
449
+ const feinaiPath = dbPath?.startsWith(home)
450
450
  ? `~${dbPath.slice(home.length)}`
451
451
  : (dbPath ?? null);
452
452
  return json({
453
453
  repoRoot: repoRoot ?? null,
454
454
  repoName: repoRoot ? basename(repoRoot) : null,
455
- tascaPath,
455
+ feinaiPath,
456
456
  });
457
457
  }
458
458
 
@@ -114,7 +114,7 @@ export function getSqliteConstructor(): DbConstructor {
114
114
 
115
115
  if (!_constructor) {
116
116
  console.error(`
117
- Error: No SQLite backend found. tasca requires one of:
117
+ Error: No SQLite backend found. feinai requires one of:
118
118
  • Bun 1.0+ (current runtime)
119
119
  • Node.js 22.5+ (built-in node:sqlite)
120
120
  • better-sqlite3 (npm install -g better-sqlite3)