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 +14 -14
- package/README.md +12 -14
- package/package.json +1 -1
- package/skills/feinai-dispatch/SKILL.md +2 -2
- package/skills/feinai-implement/SKILL.md +6 -6
- package/skills/feinai-sdd/SKILL.md +3 -3
- package/skills/feinai-write-spec/SKILL.md +131 -26
- package/skills/feinai-write-tasks/SKILL.md +2 -2
- package/src/cli.ts +10 -10
- package/src/dashboard.html +120 -31
- package/src/db.ts +5 -44
- package/src/format.ts +3 -3
- package/src/server.ts +8 -8
- package/src/sqlite-adapter.ts +1 -1
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
|
-
- `
|
|
16
|
-
- `opengit` shipped as a binary alongside `
|
|
17
|
-
- `
|
|
18
|
-
- `
|
|
19
|
-
- Live Agents Monitor: working directory, repo name, and `.
|
|
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
|
-
- `
|
|
23
|
-
- Skills: `
|
|
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
|
-
- `
|
|
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
|
-
`
|
|
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
|
-
- `
|
|
41
|
-
- `
|
|
42
|
-
- `
|
|
43
|
-
- `
|
|
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: `
|
|
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: `.
|
|
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 `.
|
|
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
|
|
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
|
```
|
|
@@ -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 .
|
|
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 .
|
|
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>/.
|
|
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
|
@@ -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 .
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 `.
|
|
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
|
-
Run `feinai status` (exit 0 =
|
|
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 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
|
|
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.
|
|
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
|
|
385
|
-
rmSync(
|
|
386
|
-
console.log(`Destroyed ${
|
|
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
|
|
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
|
|
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(`
|
|
816
|
-
console.log(`Stop with:
|
|
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(`
|
|
825
|
-
console.log(`Stop with:
|
|
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", () => {
|
package/src/dashboard.html
CHANGED
|
@@ -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>
|
|
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
|
-
|
|
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>
|
|
759
|
+
<footer>feinai · streaming live · click any item for details & 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("
|
|
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>
|
|
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("
|
|
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-
|
|
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
|
|
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>
|
|
@@ -1456,7 +1508,7 @@
|
|
|
1456
1508
|
: '';
|
|
1457
1509
|
const workingDir = task.worktree || appContext.repoRoot || "—";
|
|
1458
1510
|
const repoName = appContext.repoName || "—";
|
|
1459
|
-
const
|
|
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(
|
|
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
|
-
|
|
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
|
|
package/src/db.ts
CHANGED
|
@@ -1,47 +1,10 @@
|
|
|
1
|
-
import { existsSync, mkdirSync
|
|
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
|
|
208
|
-
const dbPath = join(
|
|
168
|
+
const feinaiDir = join(dir, DB_DIR);
|
|
169
|
+
const dbPath = join(feinaiDir, DB_FILE);
|
|
209
170
|
|
|
210
|
-
if (!existsSync(
|
|
211
|
-
mkdirSync(
|
|
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;
|
|
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;
|
|
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 (
|
|
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-
|
|
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-
|
|
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 --
|
|
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 --
|
|
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-
|
|
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
|
|
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
|
|
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
|
-
|
|
455
|
+
feinaiPath,
|
|
456
456
|
});
|
|
457
457
|
}
|
|
458
458
|
|
package/src/sqlite-adapter.ts
CHANGED
|
@@ -114,7 +114,7 @@ export function getSqliteConstructor(): DbConstructor {
|
|
|
114
114
|
|
|
115
115
|
if (!_constructor) {
|
|
116
116
|
console.error(`
|
|
117
|
-
Error: No SQLite backend found.
|
|
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)
|