TASKS · tasks.md
Tasks: speckit-eye — Zero-Setup Spec Kit Progress Dashboard
Raw markdown
---
description: "Task list for speckit-eye — Zero-Setup Spec Kit Progress Dashboard"
---
# Tasks: speckit-eye — Zero-Setup Spec Kit Progress Dashboard
**Input**: Design documents from `specs/001-speckit-eye-dashboard/`
**Prerequisites**: [plan.md](./plan.md), [spec.md](./spec.md), [research.md](./research.md), [data-model.md](./data-model.md), [contracts/](./contracts/), [quickstart.md](./quickstart.md)
**Tests**: REQUIRED. Constitution §IV (unit tests for all code) and §V (E2E coverage of every user story) are non-negotiable. Each logic task names its unit test file and ships the module and its tests together; each story phase has its own Playwright suite. E2E test names MUST start with the story/requirement ids they cover, for example `US2 FR-027 keeps scroll and expanded items on live update` (§V traceability).
**Organization**: Tasks are grouped by user story so each story can be implemented and tested as its own increment.
## Format: `[ID] [P?] [Story] Description`
- **[P]**: Can run in parallel (different files, no dependency on an unfinished task)
- **[Story]**: The user story the task belongs to (US1–US4)
- All paths are relative to the repository root
## Conventions that apply to every task
- Plain JavaScript ES modules with JSDoc, Node.js ≥ 22, no TypeScript (research R1).
- All HTML is produced with the `html` tagged template from `src/render/html.js`; only trusted fragments go through `raw()` (R4).
- Unit tests use `node:test` + `node:assert/strict`, live in `tests/unit/`, and never touch the real file system, network, or a browser (§IV). All I/O is injected: `fs` into `createReader` (T009) and `writeSite` (T053), `http.createServer` into `startServer` (T028), `fs.watch`/timers into the watcher (T035), and `document`/`window`/`EventSource`/`fetch`/`DOMParser` into the client scripts (T026, T039). File input for scan/model tests goes through the fake reader from T009. Only one-line entry points with no logic (`bin/speckit-eye.js`, the client bootstraps) are left to the E2E tests.
- The target project is never written (FR-006). Only `src/project/reader.js` reads input files; only `src/build/build.js` writes output files.
- Status colors exist once, as CSS custom properties in `src/styles/input.css` (§III).
- Every user-visible change adds a line under `## [Unreleased]` in `CHANGELOG.md` (§VI).
---
## Phase 1: Setup (Shared Infrastructure)
**Purpose**: Package, tooling, CI and developer docs skeleton
- [X] T001 Create `package.json` at the repo root: `"name": "speckit-eye"`, `"version": "0.1.0"` (the only place the version is defined, §III), `"type": "module"`, `"bin": {"speckit-eye": "bin/speckit-eye.js"}`, `"engines": {"node": ">=22"}`, `"files": ["bin/", "src/", "!src/styles/", "dist/styles.css", "README.md", "CHANGELOG.md", "LICENSE"]`, `"dependencies": {"markdown-it": "^15"}`, `"devDependencies": {"tailwindcss": "^4", "@tailwindcss/cli": "^4", "@tailwindcss/typography": "^0.5", "@playwright/test": "^1.63"}`, scripts `build:css` = `tailwindcss -i src/styles/input.css -o dist/styles.css --minify`, `prepack` = `npm run build:css`, `test` = `node --test "tests/unit/**/*.test.js"`, `test:coverage` = `node --test --experimental-test-coverage "tests/unit/**/*.test.js"`, `test:e2e` = `npm run build:css && playwright test`; then run `npm install` to create `package-lock.json`
- [X] T002 [P] Create `.gitignore` with `node_modules/`, `dist/`, `test-results/`, `playwright-report/`, `coverage/`
- [X] T003 [P] Create `src/styles/input.css` with `@import "tailwindcss";`, `@plugin "@tailwindcss/typography";`, `@source "../render";` and `@source "../client";`, a `:root` block defining `--status-done` (green), `--status-active` (highlight), `--status-started` (blue), `--status-not-started` (dark grey, also used for `future` tasks), `--status-blocked`, and a global `@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation: none !important; transition: none !important; } }` rule (FR-036); confirm `npm run build:css` writes `dist/styles.css`
- [X] T004 [P] Create `playwright.config.js`: `testDir: "tests/e2e"`, one project using Chromium only (R7), `workers: 1` (all E2E servers use the default port 4747 and US2 AC7 restarts on the same address), `retries: process.env.CI ? 1 : 0`, `reporter: [["list"], ["html", {open: "never"}]]`, `timeout: 30_000`
- [X] T005 [P] Create `CHANGELOG.md` in Keep a Changelog 1.1.0 format with an intro paragraph stating the project follows SemVer and an empty `## [Unreleased]` section (§VI)
- [X] T006 [P] Create `.github/workflows/ci.yml`: on push and pull_request; job `unit` on ubuntu-latest with a Node `[22, 24]` matrix running `npm ci` and `npm test`; job `e2e` with an OS matrix `[ubuntu-latest, macos-latest, windows-latest]` on Node 22 running `npm ci`, `npx playwright install --with-deps chromium`, `npm run test:e2e`, and uploading `playwright-report/` on failure (plan risk: watcher on macOS/Windows)
- [X] T007 [P] Create `DEVELOPMENT.md` skeleton with sections Setup (Node ≥ 22, `npm ci`, `npx playwright install chromium`), Build (`npm run build:css`), Tests (`npm test`, `npm run test:coverage`, `npm run test:e2e`, E2E naming rule `USn FR-xxx …`), Conventions (the list under "Conventions that apply to every task" above), and placeholder headings "Project structure" and "Release process" to be completed in T069 (§IX)
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Pure input → model pipeline and shared rendering primitives that every story uses
**⚠️ CRITICAL**: No user story work can begin until this phase is complete
- [X] T008 [P] Implement `src/render/html.js`: export `escapeHtml(s)` (escapes `& < > " '`), `raw(s)` (wraps a trusted string in a `Raw` object), and the tagged template `html` that escapes every interpolated value except `Raw` values, flattens arrays (joined with `""`), renders `null`/`undefined`/`false` as `""`, and returns a `Raw`; add `tests/unit/html.test.js` covering escaping, nesting, arrays, and falsy values
- [X] T009 [P] Implement the ProjectReader boundary: `src/project/reader.js` exports `createReader(root, fs)` (`fs` defaults to `node:fs/promises`; the parameter takes any object with `readdir`, `readFile`, `stat`) returning `{ list(relDir) → Promise<{name, isDir}[]> ([] when the folder is missing), read(relPath) → Promise<string> (decodes with `new TextDecoder("utf-8", {fatal: true})` so invalid UTF-8 throws), exists(relPath) → Promise<boolean>, readGitHead() → Promise<{ gitDir, head } | null> }`; every path goes through a pure exported helper `resolveInside(root, relPath)` that throws when the resolved path leaves `root`, except `readGitHead`, which reads `.git/HEAD`, or, when `.git` is a file containing `gitdir: <path>` (worktree), reads exactly the file `HEAD` inside that gitdir (the only read allowed outside `root`, research R9), returning null when neither exists; create `tests/unit/fake-reader.js` exporting `createFakeReader(files, { gitHead })` where `files` is `{ "specs/001-x/spec.md": "…", "bad.md": new Error("EIO") }` implementing the same interface; add `tests/unit/reader.test.js` testing `resolveInside` (including `..` and absolute paths), `createReader` with an in-memory fake `fs` (missing folder → `[]`, invalid UTF-8 throws, `exists`, `readGitHead` for a normal repo, a worktree `.git` file, and no repo), and the fake reader
- [X] T010 [P] Implement `src/project/artifacts.js` per data-model.md "Artifact" and contracts/routes.md: `isValidName(name)` (`/^[A-Za-z0-9._-]+$/`, W11 otherwise), `classifyKind(source)` → one of `spec | plan | research | data-model | quickstart | tasks | contract | checklist | constitution | assessment | other`, `sortFeatureArtifacts(list)` (order: spec, plan, research, data-model, quickstart, tasks, contracts by name, checklists by name, then the rest by path), `sortAssessmentArtifacts(list)` (intake, research, problem, concept, decision first, then the rest by name), `artifactTitle(markdown, fileName)` (first `# ` heading else file name), `sourceToUrl(source)` (`.specify/memory/constitution.md` → `constitution.html`; `specs/<dir>/<rel>.md` → `features/<dir>/<rel>.html`; `.specify/assessments/<slug>/<rel>.md` → `assessments/<slug>/<rel>.html`); add `tests/unit/artifacts.test.js`
- [X] T011 Implement `src/project/scan.js` (depends on T009, T010): export `isSpecKitProject(reader)` (true when `specs/` or `.specify/` exists) and `scan(reader, projectName)` returning `{ name, features: [{ dir, files: Map<relPath, content> }] (direct subfolders of specs/ sorted by plain string compare, FR-008; every *.md recursively), constitution: {source, content} | null, assessments: [{ slug, files: Map }], featureDirectory: string | null (basename of `feature_directory` from `.specify/feature.json`; W10 when unreadable or naming a missing feature), gitBranch: string | null (from `reader.readGitHead()` `ref: refs/heads/<branch>`; null on detached HEAD or no repo, R9), gitDir: string | null (for the watcher, T035), warnings }`; skip names failing `isValidName` with W11 `name not supported for a page (skipped)`, and files whose read throws with W9 `could not read file (skipped)`; add `tests/unit/scan.test.js` using the fake reader (worktree `.git` file, detached HEAD, bad names, unreadable file, missing `specs/`)
- [X] T012 [P] Implement `src/parse/tasks.js` exactly per contracts/tasks-md-format.md: `parseTasks(text, file)` → `{ phases: [{number, title, tasks}], tasks, warnings }`; split on `\n` and strip trailing `\r`; skip fenced blocks (``` and `~~~`) and HTML comments including multi-line ones; phase regex `^##\s+Phase\s+(\d+)\s*:\s*(.+?)\s*$`; task regex `^\s*[-*]\s+\[( |x|X)\]\s+(.*)$`; ID `^(T\d+)\b`; `[P]` and `[USn]` markers directly after the ID in any order; dependencies `/depends on\s+(T\d+(?:\s*(?:,\s*and|,|and|&)\s*T\d+)*)/i` (accepts `T1, T2`, `T1 and T2`, `T1, and T2`, `T1 & T2`); tasks before the first phase go into a synthetic phase with `number: null`, title `Unphased` (W3); raise W1, W2, W3, W5, W6 (drop unknown dependency IDs), W8, W12 (repeated phase number) with the contract's exact messages and 1-based line numbers; every task line counts (M5); never throw; add `tests/unit/parse-tasks.test.js` covering every table row, every warning, CRLF, fences, comments, and "total equals number of checkboxes outside code and comments"
- [X] T013 [P] Implement `src/parse/spec.js`: `parseSpec(text)` → `{ title: string | null, stories: [{label: "US<n>", title, priority}] }` using `^#\s+Feature Specification:\s*(.+)$` (first match) and `^###\s+User Story\s+(\d+)\s*[-–—]\s*(.+?)\s*\(Priority:\s*(P\d+)\)\s*$`, skipping fenced blocks; add `tests/unit/parse-spec.test.js` (hyphen, en dash, em dash, missing priority line ignored)
- [X] T014 Implement `src/model/build-model.js` (depends on T011–T013): `buildModel(scanResult)` → `Project` per data-model.md; Feature `title` from spec title else `dir`; Phase fields `number`, `title`, `tasks`, `storyLabels` (first-appearance order), `mergedStory` (set when every task carries the same single label), `groups` (only when `storyLabels.length > 1`; unlabeled tasks stay on the phase), `key` = `<featureDir>/p<number or 'u'>`, with `@L<line>` appended to every occurrence of a repeated phase number; StoryGroup `key` = `<phaseKey>/<label>`; Task `key` = `<featureDir>/<id or 'L'+line>`, with `@L<line>` appended to every occurrence of a duplicated ID, so all keys are unique (data-model.md); `Counts = { done, total, open = total - done, percent = total ? min(round(done*100/total), open ? 99 : 100) : 0 }`; `Totals = { tasks: Counts, specs: {completed, total}, phases: {completed, total} }` where a spec is completed when `hasTasks && counts.total > 0 && counts.open == 0` and every feature counts toward `specs.total`, and a phase is completed when `counts.total > 0 && counts.open == 0`, with only phases where `counts.total > 0` counted in `phases.total`; Stage per the data-model table (`empty`, `specified`, `planned`, `ready`, `in-progress`, `complete`; `tasks.md` with `total == 0` stays `planned`/`specified`/`empty`); W4 `story label USn has no matching user story in spec.md`; build artifact lists with `sortFeatureArtifacts`/`sortAssessmentArtifacts`; add `tests/unit/build-model.test.js` including 40/65 → 62 %, 199/200 → 99 %, merged vs grouped phases, stage for every row, a phase with no tasks excluded from `phases.total`, and unique keys for duplicate task IDs and repeated phase numbers
- [X] T015 Implement `src/model/active.js` (depends on T014): `selectActive(project, {featureDirectory, gitBranch})` → `{ featureDir, phaseKey, storyLabel, nextTaskKey, source: "feature.json" | "git-branch" | "first-open" | "none" }` following data-model.md "ActiveSelection" rules 1–6 verbatim (while any feature has open tasks, a candidate is skipped when `hasTasks && open == 0`; when no feature has open tasks, candidate A else B stays active even if complete, with null phase/story/task, else `none` (FR-018); feature without `tasks.md` can be active with null phase/task; `storyLabel` only when the active phase has groups; next task = first open task in file order within the active group, or within the phase when it has no groups); add `tests/unit/active.test.js` with one test per rule plus spec US1 AC6 both cases and AC7 both cases (all done without feature.json → `none`; all done with feature.json naming a feature → that feature, `source: "feature.json"`, null phase/story/task)
- [X] T016 Implement `src/model/task-state.js` (depends on T015): `applyTaskStates(project)` sets each task's `state` in the order completed → current (`key == active.nextTaskKey`) → blocked (any `dependsOn` task open) → future, adds W7 `next task Tnnn depends on open task Tmmm` when the current task has an open dependency, and sets `sig` on the project, every feature, phase, group, and task (short string from counts, stage/state, and the active flag, data-model.md "Change signature"); make `buildModel` call `selectActive` and `applyTaskStates` so it returns a complete Project; add `tests/unit/task-state.test.js`
- [X] T017 [P] Implement `src/cli/args.js` per contracts/cli.md: `parseCliArgs(argv)` using `util.parseArgs` strict mode with options `serve`, `build`, `out`, `base`, `help`/`-h`, `version`/`-v`; returns `{mode: "serve" | "build" | "help" | "version", dir, out, base}` or `{error: string}` for unknown options, both modes, no mode, `--out` missing in build, `--out`/`--base` with serve; `normalizeBase` turns `repo` → `/repo/`, default `/`; export a `USAGE` string with the synopsis; add `tests/unit/args.test.js`
- [X] T018 Implement `src/render/layout.js` (depends on T008): `renderPage({ title, base, mode, project, main, version, generatedAt })` returns a full HTML document: `<body data-mode="serve|static" data-version="…">`, `<header data-region="header">` with the project name (links to `{base}index.html`) and links to the constitution and each assessment's first artifact at `{base}` + url, `<link rel="stylesheet" href="{base}assets/styles.css">`, `<script src="{base}assets/overview.js" defer>`, the given `main`, and `<footer>` with the version; all links absolute with `base` (R10); add `tests/unit/layout.test.js`
**Checkpoint**: `npm test` passes; `buildModel(scan(fakeReader))` yields a complete Project for any fixture
---
## Phase 3: User Story 1 - See where the project stands at a glance (Priority: P1) 🎯 MVP
**Goal**: `speckit-eye --serve <dir>` prints a local address whose overview shows the header, overall bar and counters, the feature tree with only the active chain expanded, and the task grid.
**Independent Test**: Serve `tests/fixtures/projects/mixed`; the overall bar reads 40 / 65 (62 %), per-feature/phase/story counts match the fixture checkboxes, only the active chain is open, the next task is shown, and the project folder is unchanged.
### Fixtures for User Story 1
- [X] T019 [P] [US1] Create fixture `tests/fixtures/projects/mixed/`: `.specify/memory/constitution.md`; `.specify/assessments/speckit-dashboard/intake.md` and `decision.md`; `specs/001-alpha/` (spec.md with US1–US2, plan.md, tasks.md with 30/30 done); `specs/002-beta/` (spec.md with US1–US3, plan.md, tasks.md with 10/20 done: Phase 1 Setup unlabeled, Phase 3 all `[US1]` (merged row), Phase 4 mixing `[US2]`, `[US3]` and one unlabeled task, one open task whose description says `depends on T0xx` naming an open task so it is blocked); `specs/003-gamma/` (spec.md, plan.md, tasks.md with 0/15 done); `specs/004-delta/` (spec.md only → stage `specified`); document expected counts per feature/phase/story in `tests/fixtures/projects/README.md`
- [X] T020 [P] [US1] Create fixtures `tests/fixtures/projects/complete/` (two features, every task `[x]` or `[X]`, constitution) and `tests/fixtures/projects/empty/` (empty `specs/` kept with a `.gitkeep`, plus `.specify/memory/constitution.md`); add their expectations to `tests/fixtures/projects/README.md`
- [X] T021 [P] [US1] Create fixture `tests/fixtures/projects/nonstandard/specs/001-odd/` with spec.md and a tasks.md that triggers W1, W2, W3, W4 (`[US9]`), W5, W6, W7, a duplicated task ID and a repeated `## Phase 2:` heading (W12), plus a checkbox inside a fenced block and inside an HTML comment (both not counted); add `specs/002-emptytasks/tasks.md` with no task lines (W8); record expected totals and warning codes/lines in `tests/fixtures/projects/README.md`
### Implementation for User Story 1
- [X] T022 [US1] Implement the progress section in `src/render/overview.js`: `renderOverview(project, {base})` returns the `<main>` inner HTML beginning with `<section data-region="progress">` containing the overall bar as `<progress data-key="project" data-sig value="<done>" max="<total>">` (no inline `style` attribute anywhere, since the serve-mode CSP blocks them; contracts/routes.md) with the text "done / total tasks (pct %)", three counters `[data-counter="specs|phases|tasks"]` each "completed / total", a "Next: T012 · description" line for `active.nextTaskKey` (FR-017), an "All tasks are complete" message when `totals.tasks.total > 0 && totals.tasks.open == 0`, whether or not a feature is active (FR-018), and a short explanation instead of an empty page when there are no features (edge case "Empty project"); add `tests/unit/overview.test.js`
- [X] T023 [US1] Add the feature tree to `src/render/overview.js` (`<div data-region="tree">`): one `<details data-key data-sig data-status="done|started|not-started" [data-active]>` per feature in folder order with `<summary>` = title · stage label · "N open / M" (FR-019a); `open` only on the active feature, active phase, and active group (FR-016); completed items are never `open`, except a completed active feature when all tasks are done (FR-018), which gets `open` and `data-active` while its phases stay closed; phases as nested `<details>` titled "Phase N · USn – Title (Pn)" when `mergedStory` is set, otherwise "Phase N: Title" with a group sub-level per story and unlabeled tasks directly under the phase (FR-015a); tasks as `<li data-key data-state>` with ID, description, and a "next" mark on the current task; a warning block on the feature listing `file:line message` (FR-019); extend `tests/unit/overview.test.js` (including the completed active feature: `data-status="done"`, `data-active`, `open`, no open phase, no "next" mark)
- [X] T024 [US1] Add the task grid to `src/render/overview.js` (`<div data-region="grid">`): one `<a tabindex="0" data-key data-state="completed|current|blocked|future" data-parents="<featureDir> <phaseKey> [groupKey]" title="T012 · description — feature › phase">` per task in tree order, no `href` until the click decision in T034 (FR-015b); extend `tests/unit/overview.test.js`
- [X] T025 [US1] Style the overview in `src/styles/input.css`: the `<progress>` bar (track and value via `::-webkit-progress-bar`, `::-webkit-progress-value`, `::-moz-progress-bar`); tree status colors from the `--status-*` tokens (`done` green with a ✓ mark, `[data-active]` highlighted and combinable with `done`, `started` blue, `not-started` dark grey); `details` open/close animation under `motion-safe`; grid of small squares colored by `data-state` using the same tokens; two-column layout from the `md` breakpoint, stacked tree-first below it (FR-015, FR-035); a `[data-highlight]` style for tree items highlighted from the grid
- [X] T026 [P] [US1] Implement `src/client/overview.js`: export `parentKeys(dataParents)` and `attachHover(doc)`, which registers `mouseover`/`focusin` on `[data-region="grid"]` to set `data-highlight` on the tree elements whose `data-key` is listed in the target cell's `data-parents`, and clears them on `mouseout`/`focusout`; the file ends with the one-line bootstrap `if (typeof document !== "undefined") attachHover(document);`; add `tests/unit/client-overview.test.js` covering `parentKeys` and `attachHover` with small fake element/document objects (`addEventListener`, `querySelector`, `setAttribute`, `removeAttribute`)
- [X] T027 [US1] Implement `src/render/site.js`: `renderSite(project, { base, mode, version, generatedAt, assets })` → `Map<path, {type, body}>` containing `index.html` (layout + overview) and `assets/styles.css` and `assets/overview.js` from the passed-in `assets` strings (site.js stays pure; callers read the files); this is the only place that decides which pages exist (FR-031, §III); add `tests/unit/site.test.js`
- [X] T028 [US1] Implement the server in two modules. `src/serve/handler.js`: `createHandler({ getSite })` returns a pure `(req, res)` function: map `/` to `index.html`, strip the leading `/`, look the path up in `getSite()`; unknown paths (including any containing `..` or encoded variants) → 404 without touching the file system (FR-007); only GET and HEAD (others → 405); headers `Content-Type` by extension, `Cache-Control: no-store`, `X-Content-Type-Options: nosniff`, `Content-Security-Policy: default-src 'self'; style-src 'self'; script-src 'self'; img-src 'self' data:`; add `tests/unit/handler.test.js` with fake `req`/`res` objects (routes, 404 cases, 405, HEAD without body, every header). `src/serve/server.js`: `startServer({ handler, host: "127.0.0.1", port: 4747, createServer = http.createServer })` only listens: bind to `host`, on `EADDRINUSE` retry once with port 0, resolve with the actual `{ url, close }`; add `tests/unit/server.test.js` with a fake `createServer` whose `listen` emits `EADDRINUSE` or succeeds (no socket is opened, §IV)
- [X] T029 [US1] Implement serve mode in `src/cli/main.js`: `run(argv, { stdout, stderr, createReader, startServer, readAsset, onSignal })` with real defaults; handle `help` (usage to stdout, 0), `version` (from `package.json` via `import pkg from "../../package.json" with { type: "json" }`, 0), usage errors (usage to stderr, 2), missing dir or not a Spec Kit project (message naming the expected `specs/` or `.specify/`, 2); then scan → buildModel → renderSite(mode `serve`, base `/`) → `createHandler` → `startServer`; print `speckit-eye <version> — serving <abs dir>`, ` Local: <url>`, ` Watching specs/ and .specify/ for changes (Ctrl+C to stop)` and each warning to stderr as `warning: <file>:<line> <message>`; exit 0 on SIGINT/SIGTERM, 1 on unexpected errors (contracts/cli.md); add `tests/unit/main.test.js` with injected fakes
- [X] T030 [US1] Create `bin/speckit-eye.js` with `#!/usr/bin/env node`, importing `run` from `../src/cli/main.js` and setting `process.exitCode` from its result; mark it executable (`chmod +x`, and `git update-index --chmod=+x`)
- [X] T031 [US1] Create `tests/e2e/helpers.js`: `copyFixture(name)` copies `tests/fixtures/projects/<name>` to a fresh `os.tmpdir()` folder; `startServe(dir)` spawns `node bin/speckit-eye.js --serve <dir>`, resolves `{ url, stdout, stderr, stop() }` once a `Local:` line appears (15 s timeout), and fails at once with `E2E tests need port 4747 free` when that line shows another port (the server fell back because 4747 was busy); `runCli(args)` resolves `{ code, stdout, stderr }`; `hashTree(dir)` returns a digest of every file path + content for FR-006 checks
- [X] T032 [US1] Write `tests/e2e/us1-overview.spec.js` covering spec US1 AC1–AC10 one test each (AC1 also asserts the bar's `value`/`max` are 40/65; AC6 writes `.specify/feature.json` into the temp copy; AC7 uses `complete` twice: without `feature.json` nothing is active, and with `feature.json` naming its second feature that feature is `open`, `data-active`, `data-status="done"`, no phase is open, no "Next" line, and the complete message is shown; AC9 hovers a grid square and asserts the `title` and `data-highlight` on its feature and phase), plus `US1 SC-001` (at a 1280×720 viewport the progress section, the active feature's summary, and the "Next" line are inside the viewport without scrolling), `US1 FR-015c` (the dependent task in `mixed` has `data-state="blocked"` in the grid and the tree), `US1 FR-036` (a context with `reducedMotion: "reduce"`: computed `transition-duration` and `animation-name` of the bar and a `details` element are `0s`/`none`), `US1 FR-038` (every request made while loading and using the overview goes to the server's own origin, and no `securitypolicyviolation` event fires), `US1 FR-013` (nonstandard fixture: server keeps running, warnings shown on the feature and on stderr, totals equal checkbox counts), `US1 FR-015` (narrow viewport 375 px stacks tree above grid), empty project (0 % with explanation), `FR-005` (missing dir and non-Spec-Kit dir exit 2, no args exit 2, `--help` exit 0), and `FR-006` (`hashTree` unchanged after the run)
- [X] T033 [US1] Create `docs/usage.md` (serve mode: `npx speckit-eye --serve <dir>`, what the overview shows, stage labels, status colors, active-item rules FR-014 in plain words, link to [contracts/cli.md](../specs/001-speckit-eye-dashboard/contracts/cli.md) for options/exit codes) and `README.md` (what it is, `npx speckit-eye --serve .` example, prerequisites Node ≥ 22, links to `docs/` and `DEVELOPMENT.md`; no developer content, §VIII); add "Added: serve mode with overview" to `CHANGELOG.md`
- [X] T034 [US1] Visual review (owner gate — needs a human decision): run `node bin/speckit-eye.js --serve tests/fixtures/projects/mixed` and against this repository, present screenshots, ask the owner (a) what clicking a tree item or grid square does and (b) the threshold and fallback for very large grids; record the decisions in `specs/001-speckit-eye-dashboard/spec.md` under Assumptions "Open design items" and append implementation tasks for them to this file after T070
**Checkpoint**: US1 is a usable MVP: `npm test` and `npx playwright test tests/e2e/us1-overview.spec.js` pass
---
## Phase 4: User Story 2 - Follow progress live during a long run (Priority: P2)
**Goal**: In serve mode, every open page updates within ~2 s of a Spec Kit Markdown file settling, keeping scroll and expanded items, highlighting changed items, and showing a banner while disconnected.
**Independent Test**: Serve a temp copy of `mixed`, expand a non-active feature and scroll, tick a task on disk; within 2 s the counts update, scroll and expansion are unchanged, and the changed items carry `data-changed`.
### Implementation for User Story 2
- [X] T035 [P] [US2] Implement `src/serve/watcher.js`: `createWatcher({ root, gitDir, watch = fs.watch, exists, setTimeout, clearTimeout, onChange, onError })` watching `specs/`, `.specify/memory/`, `.specify/assessments/` with `{ recursive: true }`; the single-file inputs through their parent folders without recursion, filtered by file name (`.specify/` → `feature.json`; `gitDir` → `HEAD`), because rename-saves make a watch on the file itself go silent (R2); and the project root and `.specify/` without recursion so that a recursive root created after startup (`specs`, `memory`, `assessments`) is added on the next event (`refreshRoots()` is idempotent and uses the injected `exists`); missing roots are skipped until then; any relevant event triggers a debounced `onChange()` with a 100 ms quiet period and a 500 ms maximum wait during bursts (R2); watcher `error` events go to `onError` and do not crash; `close()` stops all watchers and timers; add `tests/unit/watcher.test.js` with a fake `watch`, a fake `exists`, and `node:test` mock timers (single event, burst capped at 500 ms, `HEAD` replaced by rename still fires, unrelated file in `.git/` ignored, `specs/` created after start gets watched, close)
- [X] T036 [P] [US2] Implement `src/serve/events.js`: `createEventHub({ setInterval, clearInterval })` with `add(req, res)` (writes `Content-Type: text/event-stream`, `Cache-Control: no-store`, then `event: hello\ndata: {"version": <n>}\n\n`, removes the client on `close`), `broadcast(version)` (writes `event: change\ndata: {"version": <n>}\n\n` to every client), a `: ping` comment every 25 s, and `close()`; add `tests/unit/events.test.js` with fake response objects
- [X] T037 [US2] Add `GET /__events` to `src/serve/handler.js` (`createHandler({ getSite, events })`, routed to `events.add`), exempt from the site-map lookup; extend `tests/unit/handler.test.js`
- [X] T038 [US2] In `src/render/layout.js` add, only when `mode == "serve"`, `<script src="{base}assets/live.js" defer>` and `<div data-region="live-status" hidden>Live updates paused — reconnecting…</div>`; in `src/render/site.js` add `assets/live.js` to the map only in serve mode; extend `tests/unit/layout.test.js` and `tests/unit/site.test.js` asserting both are absent in static mode
- [X] T039 [US2] Implement `src/client/live.js` per contracts/routes.md "Live-update protocol" as `createLiveClient({ document, window, EventSource, fetch, DOMParser, setTimeout, requestAnimationFrame })` with `start()`: track the viewer's own toggles (`toggle` events on `details[data-key]` → `Map<key, open>`); on `change`: record `scrollY`, `data-sig` per `data-key`, and `<progress>` values, `fetch(location.pathname)`, parse with `DOMParser`, replace `<main>`, re-apply the viewer's toggles, restore `scrollY`, set `data-changed` for 1.5 s on elements whose `data-sig` differs, animate each `<progress>` `value` from old to new with `requestAnimationFrame` (skipped when `matchMedia("(prefers-reduced-motion: reduce)")` matches); on 404 show a notice linking to the overview; on `EventSource` `error` unhide `[data-region="live-status"]`, on the next `hello` hide it and fetch once (FR-030); also export the helpers `collectSigs(elements)`, `changedKeys(oldSigs, newSigs)`, `applyToggles(detailsList, toggles)`; the file ends with the one-line bootstrap `if (typeof document !== "undefined") createLiveClient({ document, window, EventSource, fetch, DOMParser, setTimeout, requestAnimationFrame }).start();`; add `tests/unit/client-live.test.js` covering the helpers and `createLiveClient` with fake document/window/EventSource/fetch/DOMParser objects and mock timers (swap keeps toggles and scroll, `data-changed` set and cleared, value animation and its reduced-motion skip, 404 notice, banner on `error` and hidden plus one fetch on `hello`)
- [X] T040 [US2] Wire live updates in `src/cli/main.js`: create the event hub and the watcher (passing `gitDir` from the scan result, and re-creating the watcher when a rescan reports a different `gitDir`); on change rescan → buildModel → renderSite; if any page body changed, increment the model version and `broadcast(version)`; print `updated (<n> features, <done>/<total> tasks)` per rescan and each new or changed warning once; a failing rescan logs to stderr and keeps the last site; close watcher and hub on SIGINT/SIGTERM; extend `tests/unit/main.test.js` with fake watcher/hub
- [X] T041 [US2] Add live-update styles to `src/styles/input.css`: `[data-changed]` highlight animation (~1.5 s, `motion-safe` only), `[data-region="live-status"]` fixed banner, and the 404 notice
- [X] T042 [US2] Write `tests/e2e/us2-live.spec.js` covering spec US2 AC1–AC7 one test each, with ≤ 2 s assertions: AC1 tick a task with `fs.writeFile`, then repeat 20 single ticks one after another and assert at least 19 appear within 2 s (`US2 SC-002`, 95th percentile); AC2 expand a non-active feature, scroll, tick, assert `scrollY` and `open` unchanged; AC3 assert `data-changed` on the changed task/phase/feature and the bar's new `value`; AC4 artifact page update — mark `test.fixme` until US3 is done; AC5 create `specs/005-new/spec.md`, then `US2 FR-026` delete that folder and assert the feature disappears from the tree; AC6 write a temp file and `rename` over `tasks.md`, then 10 writes in 200 ms, assert the final content; AC7 stop the CLI, assert the banner is visible, restart on the same dir (same port 4747), assert the banner hides and content catches up
- [X] T043 [US2] Cross-platform watcher check (plan risk R2): confirm `us2-live.spec.js` passes in the CI e2e matrix on ubuntu, macOS, and Windows (T006), and run it once under WSL with the project on the Linux file system; record the results and date under R2 in `specs/001-speckit-eye-dashboard/research.md`; if any platform fails, stop and request a plan amendment for the `chokidar` fallback instead of adding it
- [X] T044 [US2] Add a "Live updates" section to `docs/usage.md` (what triggers an update, ~2 s latency, banner meaning, WSL note: keep the project on the Linux file system, files under `/mnt/c` edited from Windows do not raise events); add "Added: live updates in serve mode" to `CHANGELOG.md`
**Checkpoint**: US1 and US2 pass their E2E suites independently
---
## Phase 5: User Story 3 - Read any artifact in two steps or fewer (Priority: P2)
**Goal**: Every Markdown artifact in the project has a safe, well-typeset page reachable from the overview in ≤ 2 steps via the menu or feature links.
**Independent Test**: Serve the `artifacts` fixture; every artifact opens within two clicks from the overview, tables/task lists/code render, relative links go to rendered pages, and no script from a file runs.
### Implementation for User Story 3
- [X] T045 [P] [US3] Create fixture `tests/fixtures/projects/artifacts/`: `.specify/memory/constitution.md`; `.specify/assessments/idea-x/` with `intake.md`, `decision.md`, `notes.md`; `specs/001-full/` with `spec.md`, `plan.md`, `research.md`, `data-model.md`, `quickstart.md`, `tasks.md`, `contracts/cli.md`, `checklists/requirements.md` (with checkboxes that must not count), `decisions.md`, `run-log.md`, and `bad name.md` (W11); `specs/002-partial/` with only `spec.md` and `plan.md`; `plan.md` must contain a table, a task list, a fenced `js` block, a ` ```mermaid ` block, `<script>alert(1)</script>`, `<img src=x onerror=alert(1)>`, `[spec](./spec.md)`, `[cli](./contracts/cli.md#synopsis)`, `[src](../../src/index.js)`, `[x](javascript:alert(1))`, and `[docs](https://example.com)`
- [X] T046 [P] [US3] Implement `src/render/markdown.js`: `createMarkdown({ artifactsBySource, base })` returns `render(source, text)` using `markdown-it` with `html: false`, `linkify: false` (R3); an in-house core rule turning list items starting with `[ ]`/`[x]`/`[X]` into `<input type="checkbox" disabled>` (checked when done); link rewriting: resolve relative hrefs against the source file's folder, keep the `#fragment`, map known artifact sources to `{base}` + url, keep `http:`, `https:`, `mailto:` links, render every other relative link as plain text (FR-023); heading `id`s from an in-house slug (lowercase, runs of non-`[a-z0-9]` → `-`, trimmed, duplicates suffixed `-1`, `-2`); fenced blocks (including `mermaid`) rendered as escaped `<pre><code class="language-x">`; add `tests/unit/markdown.test.js` covering script/img escaping, `javascript:` rejected, table, task list, link rewrite cases, slugs
- [X] T047 [US3] Implement `src/render/artifact.js`: `renderArtifact(artifact, bodyHtml, { base, feature })` → `<main>` containing a breadcrumb (Overview › feature title › artifact title) and `<article class="prose" data-region="artifact" data-key="<source path>">`; add `tests/unit/artifact.test.js`
- [X] T048 [US3] Add the site-wide menu to `src/render/layout.js`: a `<details data-region="menu">` in the header (works without scripts) listing Overview, Constitution, each assessment with its artifacts, and each feature with its artifacts in data-model order, plus an always-visible "Overview" link on every page (FR-022, FR-025); extend `tests/unit/layout.test.js`
- [X] T049 [US3] In `src/render/overview.js` list links to each present artifact (title, in data-model order) inside every feature's `<details>`, above its phases; no link for missing artifacts (US3 AC1, AC5); extend `tests/unit/overview.test.js`
- [X] T050 [US3] Extend `src/render/site.js` to add one page per artifact (constitution, every feature artifact, every assessment artifact) using `createMarkdown` and `renderArtifact`, keyed by its url; extend `tests/unit/site.test.js` asserting the page set equals the artifact set and W11 names are absent
- [X] T051 [US3] Write `tests/e2e/us3-artifacts.spec.js` covering spec US3 AC1–AC6 one test each, plus `US3 SC-004 FR-022` (breadth-first crawl from the overview following at most 2 link clicks reaches every artifact page listed in the fixture), `US3 FR-024` (no dialog event fires on the plan page; `<script>` appears as text), `US3 FR-023` (the `../../src/index.js` link is plain text and `/src/index.js` returns 404), and `US3 FR-020` (mermaid block shown as code); `US3 FR-038` (every request while loading artifact pages goes to the server's own origin); then remove `test.fixme` from US2 AC4 in `tests/e2e/us2-live.spec.js` and make it pass, and add `US2 FR-026` there: with the `research.md` page open, delete the file and assert the notice linking to the overview appears within 2 s
- [X] T052 [US3] Add an "Artifact pages" section to `docs/usage.md` (which files are listed, order, menu, link behaviour, diagrams shown as code); add "Added: artifact pages and site menu" to `CHANGELOG.md`
**Checkpoint**: US1–US3 pass their E2E suites; header links to constitution/assessments now resolve
---
## Phase 6: User Story 4 - Publish a shareable snapshot from CI (Priority: P3)
**Goal**: `speckit-eye --build <dir> --out <folder> [--base /repo/]` writes the same pages as a static site that works under a sub-path, without scripts, and without live updates.
**Independent Test**: Build `mixed` with `--base /my-repo/`, serve the output under `/my-repo/` from a plain static server; overview and every artifact page load directly, links and styles work, expand/collapse works with JavaScript disabled, and no `__events` request is made.
### Implementation for User Story 4
- [X] T053 [P] [US4] Implement `src/build/build.js`: `writeSite({ site, out, projectRoot, fs })` with injected `fs/promises`-like `fs`; reject `out` equal to `projectRoot` or inside `<projectRoot>/specs` or `<projectRoot>/.specify` (usage error); if `out` exists, is not empty, and has no `.speckit-eye-build` → return `{ error, code: 2 }` having written nothing (FR-033); if the marker exists, remove the folder's contents first; write every page (creating folders), then the `.speckit-eye-build` marker; return `{ pages: <count> }`; add `tests/unit/build.test.js` with an in-memory fake fs (fresh folder, rebuild removes stale page, foreign folder untouched, out inside specs/)
- [X] T054 [US4] Static mode in `src/render/layout.js` and `src/render/site.js`: when `mode == "static"`, render `generated at <ISO time>` from `generatedAt` in the footer, omit `live.js`, the live-status banner, and anything referencing `__events`, and prefix every link and asset with `base`; add a unit test in `tests/unit/site.test.js` asserting that serve and static `<main>` HTML are identical for the same model with base `/` (FR-031)
- [X] T055 [US4] Implement build mode in `src/cli/main.js`: scan → buildModel → renderSite(mode `static`, `base`, `generatedAt = new Date().toISOString()`) → `writeSite`; print `speckit-eye <version> — building <abs dir> → <out> (base <base>)`, ` wrote <n> pages`, ` <n> warnings (see above)` when any, and the two-line public-exposure note from contracts/cli.md (FR-034); exit 0 on success, 2 for usage/marker errors, 1 when writing fails; extend `tests/unit/main.test.js`
- [X] T056 [US4] Add to `tests/e2e/helpers.js`: `runBuild(dir, out, base)` and `serveStatic(rootDir, mountPath)` — a test-only `node:http` static file server on port 0 that maps `<mountPath>*` to files under `rootDir` (directory → `index.html`, unknown → 404)
- [X] T057 [US4] Write `tests/e2e/us4-build.spec.js` covering spec US4 AC1–AC5 one test each: AC1 page count and exit 0 with summary; AC2 deep link `/my-repo/features/001-alpha/plan.html` loads with styles; AC3 overview matches serve mode (compare `[data-region=progress]`, tree and grid text), footer shows generated time, no request to `__events` and every request stays on the static server's origin (`US4 FR-038`), expand/collapse works in a context with `javaScriptEnabled: false` (`US4 FR-037`); AC4 see T059; AC5 rebuild after deleting an artifact leaves no stale page, and a foreign non-empty folder yields exit 2 with contents unchanged; plus `US4 FR-006` (`hashTree` of the project unchanged) and `US4 FR-034` (stdout contains the exposure note)
- [X] T058 [US4] Create `docs/hosting.md`: a GitHub Pages workflow sample (`actions/checkout@v4`, `actions/setup-node@v4` with Node 22, `npx speckit-eye --build . --out _site --base /${{ github.event.repository.name }}/`, `actions/upload-pages-artifact@v3`, `actions/deploy-pages@v4`, `pages: write` and `id-token: write` permissions), a prominent warning next to it that a hosted build makes the specs, research, constitution and assessments readable by anyone who can reach the site unless the host restricts access (FR-034), a note that `.specify/feature.json` is git-ignored and that CI checks out `main` (push) or a detached HEAD (pull requests), which rarely matches a feature folder name, so the active feature usually falls back to the first feature with open tasks (R9), and a short Netlify note; add a "Publish a snapshot" section to `README.md` with the build command, the same warning, and a link to `docs/hosting.md`; add build options to `docs/usage.md`; add "Added: static build mode" to `CHANGELOG.md`
- [X] T059 [US4] Add `US4 AC4 FR-039` to `tests/e2e/us4-build.spec.js`: read `docs/hosting.md`, extract the `npx speckit-eye --build …` line from the YAML sample, substitute `node bin/speckit-eye.js` and a concrete base, run it against a temp copy of `mixed`, and assert exit 0 and a loadable `index.html` under the base
**Checkpoint**: All four user stories pass their E2E suites independently
---
## Phase 7: Polish & Cross-Cutting Concerns
**Purpose**: Security and scale verification, self-check, docs, and release readiness
- [X] T060 [P] Write `tests/e2e/security.spec.js` (FR-006, FR-007, FR-024, SC-008): serving `mixed`, raw HTTP requests (via `node:http`, not the browser, so paths are not normalized) for `/../package.json`, `/%2e%2e/package.json`, `/specs/001-alpha/spec.md`, `/.specify/memory/constitution.md`, `/.env`, `/src/cli/main.js`, `/features/../../package.json` all return 404; every page response has the CSP and `nosniff` headers, and no `securitypolicyviolation` event fires on the overview or any artifact page; the server is not reachable on a non-loopback interface address from `os.networkInterfaces()` (skip when none exists); `hashTree` of the project is unchanged after serve, a live update, and a build
- [X] T061 [P] Create `tests/fixtures/generate-large.js` (`node tests/fixtures/generate-large.js <dir>` writes 50 features × 40 tasks with spec.md, plan.md, tasks.md in 4 phases, about half the tasks checked) and `tests/e2e/scale.spec.js` (`SC-010`): overview `load` event ≤ 2 s after navigation, a checkbox change visible ≤ 2 s, `--build` completes ≤ 30 s
- [X] T062 [P] Write `tests/e2e/self-counts.spec.js` (`SC-005`): build this repository's own project into a temp folder and assert, for each `specs/*/tasks.md`, that the feature's "open / total" in the built overview equals the checkbox counts computed in the test by the contract's task regex outside fenced blocks and HTML comments
- [X] T063 [P] Create `docs/architecture.md`: module list by folder with one-line responsibilities, the one-way data flow `project → parse → model → render → serve/build`, why `render/site.js` is the single page decider, the live-update sequence (watcher → rescan → version → SSE → client swap), and links to [contracts/](../specs/001-speckit-eye-dashboard/contracts/) and [data-model.md](../specs/001-speckit-eye-dashboard/data-model.md) instead of copying them (§III, §VII)
- [X] T064 Complete `README.md`: add a screenshot `docs/screenshot.png` captured with Playwright from the `mixed` fixture, a feature list, both commands, and links to `docs/usage.md`, `docs/hosting.md`, `docs/architecture.md`, `DEVELOPMENT.md`, and `CHANGELOG.md`; keep developer content out (§VIII)
- [X] T065 Run `npm run test:coverage` and add unit tests to the corresponding `tests/unit/*.test.js` files for any uncovered branch in `src/`; the only lines allowed to stay uncovered are the one-line bootstraps at the end of `src/client/*.js` (§IV)
- [X] T066 Verify packaging: run `npm pack --dry-run` and confirm the tarball contains `bin/`, `src/` without `src/styles/`, `dist/styles.css`, `README.md`, `CHANGELOG.md`, `LICENSE`, and nothing else; install the tarball in a temp folder and run `npx speckit-eye --serve <path to mixed fixture>` to confirm it starts with one runtime dependency tree (FR-001, SC-007); record the result in `DEVELOPMENT.md` release checklist
- [X] T067 Run every scenario in `specs/001-speckit-eye-dashboard/quickstart.md` (1–6) by hand and fix any mismatch between the quickstart, the docs, and the behaviour
- [X] T068 Grep `src/` for unused exports, dead code, and TODOs and remove them; confirm `package.json` `dependencies` lists only `markdown-it` (§I, §II)
- [X] T069 Complete `DEVELOPMENT.md`: "Project structure" (short tree of `src/`, `tests/`, `docs/` linking to `docs/architecture.md`) and "Release process" (update `CHANGELOG.md` `[Unreleased]` → `[X.Y.Z] - date`, bump `version` in `package.json` only, `npm pack --dry-run` check from T066, `git tag vX.Y.Z`, `npm publish`) (§VI, §IX)
- [X] T070 Prepare the first release: move the `[Unreleased]` entries in `CHANGELOG.md` to `## [1.0.0] - <date>`, set `"version": "1.0.0"` in `package.json`; leave tagging and `npm publish` to the owner (plan §VI)
### Owner decisions from the T034 visual review (spec Assumptions "Open design items")
These tasks do not depend on T070 and can run any time after US1. Do them before T065–T070 so coverage, docs and the release include them.
- [X] T071 [US1] Grid click in `src/render/overview.js`: give each tree task `<li>` an `id` built from its `data-key` (a stable, attribute-safe form such as `task-` + the key with every character outside `[A-Za-z0-9_-]` replaced by `-`, with a numeric suffix when two keys collide) and give each grid square `href="#<that id>"`, so a click jumps to the task with or without JavaScript (browsers open closed `<details>` ancestors of a fragment target); update the doc comment that says "No `href` until … T034"; extend `tests/unit/overview.test.js` (every square's `href` matches exactly one tree `id`, ids are unique for repeated task IDs)
- [X] T072 [US1] Grid click in `src/client/overview.js`: export `attachClick(doc)` (and call it from the bootstrap line next to `attachHover`), which on `click` of a grid square prevents the default jump, sets `open` on the feature, phase and group `<details>` listed in its `data-parents`, then calls `scrollIntoView({ block: "center" })` on the target task (behavior `smooth` unless `matchMedia("(prefers-reduced-motion: reduce)")` matches, FR-036) and marks it with `data-highlight` until the next click; tree items keep their native toggle and get no click handler; extend `tests/unit/client-overview.test.js` with fakes (parents opened, target scrolled, default prevented, reduced-motion uses `auto`)
- [X] T073 [US1] Large-grid fallback in `src/render/overview.js` and `src/styles/input.css`: when `totals.tasks.total` ≤ 1,000 keep the single grid; above 1,000 render `data-layout="rows"` with one row per feature (a feature label, then its squares, smaller via a `[data-layout="rows"]` style); above 5,000 render `data-layout="bars"` with one `<progress data-key="<featureDir>" value max>` bar per feature labelled "title · done / total", linking to the feature's tree item; keep `data-key`, `data-state`, `data-parents`, `title` and `href` on squares in both square layouts so hover (T026), click (T072) and live updates still work; export the thresholds as named constants; extend `tests/unit/overview.test.js` (1,000 → single grid, 1,001 → rows, 5,001 → bars) and add the new styles with no inline `style` attribute (CSP)
- [X] T074 [US1] Add E2E tests to `tests/e2e/us1-overview.spec.js`: `US1 FR-015b click` (in `mixed`, with the task's phase collapsed, clicking a grid square opens its feature, phase and story and the task is inside the viewport; the same with `javaScriptEnabled: false` reaches the task through the fragment, FR-037) and `US1 FR-015b large grid` (using `tests/fixtures/generate-large.js` from T061 with 2,000 tasks the grid has `data-layout="rows"` and one row per feature); update `docs/usage.md` (what a grid click does, how large grids are shown) and add "Added: click a grid square to jump to its task; compact grid for large projects" to `CHANGELOG.md`
- [X] T075 [US2] Add a `wsl` job to `.github/workflows/ci.yml`: on windows-latest, use `Vampire/setup-wsl` (Ubuntu 24.04, WSL 2 where the runner supports it), copy the checkout into the Linux file system, install Node 22 and pnpm, and run `tests/e2e/us2-live.spec.js`, so the WSL part of T043 runs on every push instead of by hand; record the WSL version the runner provides under R2 in `research.md`
---
## Dependencies & Execution Order
### Phase Dependencies
- **Setup (Phase 1)**: no dependencies
- **Foundational (Phase 2)**: depends on Setup (T001 for `markdown-it`/`node:test` scripts). BLOCKS all stories
- **US1 (Phase 3)**: depends on Foundational. MVP
- **US2 (Phase 4)**: depends on US1 (server, CLI serve mode, overview DOM contract, E2E helpers)
- **US3 (Phase 5)**: depends on US1 (server, site.js, layout, E2E helpers). Independent of US2 except T051 un-fixme-ing US2 AC4
- **US4 (Phase 6)**: depends on US1; its artifact deep-link checks need US3's artifact pages. Independent of US2 (live parts are simply omitted)
- **Polish (Phase 7)**: depends on all stories
### User Story Dependency Graph
```text
Setup → Foundational → US1 ─┬─→ US2 ─────────────┐
├─→ US3 ─┬───────────┼─→ Polish
│ └─→ US4 ────┘
└─→ (T034 visual review: owner gate, can run alongside US2/US3)
```
### Within Foundational
- T008, T009, T010, T012, T013, T017 are independent
- T011 needs T009 + T010; T014 needs T011–T013; T015 needs T014; T016 needs T015; T018 needs T008
### Within Each Story
- Fixtures before E2E specs; pure modules before `site.js`; `site.js` + `server.js` before `main.js`; `main.js` + `bin` before E2E specs
- Tasks editing the same file (`overview.js` T022→T023→T024→T049; `layout.js` T018→T038→T048→T054; `site.js` T027→T038→T050→T054; `main.js` T029→T040→T055; `handler.js` T028→T037) run in that order
### Parallel Opportunities
- Setup: T002–T007 in parallel after T001
- Foundational: T008, T009, T010, T012, T013, T017 in parallel
- US1: fixtures T019–T021 and T026 in parallel with T022–T025
- US2: T035 and T036 in parallel
- US3: T045 and T046 in parallel; US3 can run alongside US2 once US1 is done (different files except `layout.js`/`site.js`, so sequence T038 and T048/T050)
- Polish: T060–T063 in parallel
---
## Parallel Example: Foundational
```bash
Task: "Implement src/render/html.js with tests/unit/html.test.js" (T008)
Task: "Implement src/project/reader.js + tests/unit/fake-reader.js" (T009)
Task: "Implement src/project/artifacts.js with tests/unit/artifacts.test.js" (T010)
Task: "Implement src/parse/tasks.js with tests/unit/parse-tasks.test.js" (T012)
Task: "Implement src/parse/spec.js with tests/unit/parse-spec.test.js" (T013)
Task: "Implement src/cli/args.js with tests/unit/args.test.js" (T017)
```
## Parallel Example: User Story 1
```bash
Task: "Create fixture tests/fixtures/projects/mixed/" (T019)
Task: "Create fixtures complete/ and empty/" (T020)
Task: "Create fixture nonstandard/" (T021)
Task: "Implement src/client/overview.js hover highlight" (T026)
```
## Parallel Example: User Story 2
```bash
Task: "Implement src/serve/watcher.js with fake fs.watch + mock timers" (T035)
Task: "Implement src/serve/events.js SSE hub" (T036)
```
---
## Implementation Strategy
### MVP First (User Story 1 Only)
1. Phase 1 Setup → Phase 2 Foundational
2. Phase 3 US1 → `npm test` + `us1-overview.spec.js` green
3. **STOP and VALIDATE** with quickstart Scenario 1; run T034 visual review with the owner
### Incremental Delivery
1. Setup + Foundational → the model is correct and unit-tested
2. US1 → local overview (MVP)
3. US2 → live updates (the watcher cross-platform check T043 is the main risk; resolve before moving on)
4. US3 → artifact pages (can overlap with US2)
5. US4 → static hosting
6. Polish → security, scale, docs, release prep
---
## Notes
- [P] = different files, no dependency on an unfinished task
- Every E2E test name starts with its `USn` and/or `FR-xxx`/`SC-xxx` ids (§V)
- A task is done only when its unit tests (or E2E spec) pass and `CHANGELOG.md`/docs are updated where it says so
- T034 and T043 may stop the run for an owner decision or a plan amendment; do not work around them
- Commit after each task or logical group
01 description: "Task list for speckit-eye — Zero-Setup Spec Kit Progress Dashboard"
Input: Design documents from specs/001-speckit-eye-dashboard/
Prerequisites: plan.md, spec.md, research.md, data-model.md, contracts/, quickstart.md
Tests: REQUIRED. Constitution §IV (unit tests for all code) and §V (E2E coverage of every user story) are non-negotiable. Each logic task names its unit test file and ships the module and its tests together; each story phase has its own Playwright suite. E2E test names MUST start with the story/requirement ids they cover, for example US2 FR-027 keeps scroll and expanded items on live update (§V traceability).
Organization: Tasks are grouped by user story so each story can be implemented and tested as its own increment.
02 Format: [ID] [P?] [Story] Description
- [P]: Can run in parallel (different files, no dependency on an unfinished task)
- [Story]: The user story the task belongs to (US1–US4)
- All paths are relative to the repository root
03 Conventions that apply to every task
- Plain JavaScript ES modules with JSDoc, Node.js ≥ 22, no TypeScript (research R1).
- All HTML is produced with the
htmltagged template fromsrc/render/html.js; only trusted fragments go throughraw()(R4). - Unit tests use
node:test+node:assert/strict, live intests/unit/, and never touch the real file system, network, or a browser (§IV). All I/O is injected:fsintocreateReader(T009) andwriteSite(T053),http.createServerintostartServer(T028),fs.watch/timers into the watcher (T035), anddocument/window/EventSource/fetch/DOMParserinto the client scripts (T026, T039). File input for scan/model tests goes through the fake reader from T009. Only one-line entry points with no logic (bin/speckit-eye.js, the client bootstraps) are left to the E2E tests. - The target project is never written (FR-006). Only
src/project/reader.jsreads input files; onlysrc/build/build.jswrites output files. - Status colors exist once, as CSS custom properties in
src/styles/input.css(§III). - Every user-visible change adds a line under
## [Unreleased]inCHANGELOG.md(§VI).
04 Phase 1: Setup (Shared Infrastructure)
Purpose: Package, tooling, CI and developer docs skeleton
- T001 Create
package.jsonat the repo root:"name": "speckit-eye","version": "0.1.0"(the only place the version is defined, §III),"type": "module","bin": {"speckit-eye": "bin/speckit-eye.js"},"engines": {"node": ">=22"},"files": ["bin/", "src/", "!src/styles/", "dist/styles.css", "README.md", "CHANGELOG.md", "LICENSE"],"dependencies": {"markdown-it": "^15"},"devDependencies": {"tailwindcss": "^4", "@tailwindcss/cli": "^4", "@tailwindcss/typography": "^0.5", "@playwright/test": "^1.63"}, scriptsbuild:css=tailwindcss -i src/styles/input.css -o dist/styles.css --minify,prepack=npm run build:css,test=node --test "tests/unit/**/*.test.js",test:coverage=node --test --experimental-test-coverage "tests/unit/**/*.test.js",test:e2e=npm run build:css && playwright test; then runnpm installto createpackage-lock.json - T002 [P] Create
.gitignorewithnode_modules/,dist/,test-results/,playwright-report/,coverage/ - T003 [P] Create
src/styles/input.csswith@import "tailwindcss";,@plugin "@tailwindcss/typography";,@source "../render";and@source "../client";, a:rootblock defining--status-done(green),--status-active(highlight),--status-started(blue),--status-not-started(dark grey, also used forfuturetasks),--status-blocked, and a global@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation: none !important; transition: none !important; } }rule (FR-036); confirmnpm run build:csswritesdist/styles.css - T004 [P] Create
playwright.config.js:testDir: "tests/e2e", one project using Chromium only (R7),workers: 1(all E2E servers use the default port 4747 and US2 AC7 restarts on the same address),retries: process.env.CI ? 1 : 0,reporter: [["list"], ["html", {open: "never"}]],timeout: 30_000 - T005 [P] Create
CHANGELOG.mdin Keep a Changelog 1.1.0 format with an intro paragraph stating the project follows SemVer and an empty## [Unreleased]section (§VI) - T006 [P] Create
.github/workflows/ci.yml: on push and pull_request; jobuniton ubuntu-latest with a Node[22, 24]matrix runningnpm ciandnpm test; jobe2ewith an OS matrix[ubuntu-latest, macos-latest, windows-latest]on Node 22 runningnpm ci,npx playwright install --with-deps chromium,npm run test:e2e, and uploadingplaywright-report/on failure (plan risk: watcher on macOS/Windows) - T007 [P] Create
DEVELOPMENT.mdskeleton with sections Setup (Node ≥ 22,npm ci,npx playwright install chromium), Build (npm run build:css), Tests (npm test,npm run test:coverage,npm run test:e2e, E2E naming ruleUSn FR-xxx …), Conventions (the list under "Conventions that apply to every task" above), and placeholder headings "Project structure" and "Release process" to be completed in T069 (§IX)
05 Phase 2: Foundational (Blocking Prerequisites)
Purpose: Pure input → model pipeline and shared rendering primitives that every story uses
⚠️ CRITICAL: No user story work can begin until this phase is complete
- T008 [P] Implement
src/render/html.js: exportescapeHtml(s)(escapes& < > " '),raw(s)(wraps a trusted string in aRawobject), and the tagged templatehtmlthat escapes every interpolated value exceptRawvalues, flattens arrays (joined with""), rendersnull/undefined/falseas"", and returns aRaw; addtests/unit/html.test.jscovering escaping, nesting, arrays, and falsy values - T009 [P] Implement the ProjectReader boundary:
src/project/reader.jsexportscreateReader(root, fs)(fsdefaults tonode:fs/promises; the parameter takes any object withreaddir,readFile,stat) returning{ list(relDir) → Promise<{name, isDir}[]> ([] when the folder is missing), read(relPath) → Promise<string> (decodes withnew TextDecoder("utf-8", {fatal: true})so invalid UTF-8 throws), exists(relPath) → Promise<boolean>, readGitHead() → Promise<{ gitDir, head } | null> }; every path goes through a pure exported helperresolveInside(root, relPath)that throws when the resolved path leavesroot, exceptreadGitHead, which reads.git/HEAD, or, when.gitis a file containinggitdir: <path>(worktree), reads exactly the fileHEADinside that gitdir (the only read allowed outsideroot, research R9), returning null when neither exists; createtests/unit/fake-reader.jsexportingcreateFakeReader(files, { gitHead })wherefilesis{ "specs/001-x/spec.md": "…", "bad.md": new Error("EIO") }implementing the same interface; addtests/unit/reader.test.jstestingresolveInside(including..and absolute paths),createReaderwith an in-memory fakefs(missing folder →[], invalid UTF-8 throws,exists,readGitHeadfor a normal repo, a worktree.gitfile, and no repo), and the fake reader - T010 [P] Implement
src/project/artifacts.jsper data-model.md "Artifact" and contracts/routes.md:isValidName(name)(/^[A-Za-z0-9._-]+$/, W11 otherwise),classifyKind(source)→ one ofspec | plan | research | data-model | quickstart | tasks | contract | checklist | constitution | assessment | other,sortFeatureArtifacts(list)(order: spec, plan, research, data-model, quickstart, tasks, contracts by name, checklists by name, then the rest by path),sortAssessmentArtifacts(list)(intake, research, problem, concept, decision first, then the rest by name),artifactTitle(markdown, fileName)(first#heading else file name),sourceToUrl(source)(.specify/memory/constitution.md→constitution.html;specs/<dir>/<rel>.md→features/<dir>/<rel>.html;.specify/assessments/<slug>/<rel>.md→assessments/<slug>/<rel>.html); addtests/unit/artifacts.test.js - T011 Implement
src/project/scan.js(depends on T009, T010): exportisSpecKitProject(reader)(true whenspecs/or.specify/exists) andscan(reader, projectName)returning{ name, features: [{ dir, files: Map<relPath, content> }] (direct subfolders of specs/ sorted by plain string compare, FR-008; every *.md recursively), constitution: {source, content} | null, assessments: [{ slug, files: Map }], featureDirectory: string | null (basename offeature_directoryfrom.specify/feature.json; W10 when unreadable or naming a missing feature), gitBranch: string | null (fromreader.readGitHead()ref: refs/heads/<branch>; null on detached HEAD or no repo, R9), gitDir: string | null (for the watcher, T035), warnings }; skip names failingisValidNamewith W11name not supported for a page (skipped), and files whose read throws with W9could not read file (skipped); addtests/unit/scan.test.jsusing the fake reader (worktree.gitfile, detached HEAD, bad names, unreadable file, missingspecs/) - T012 [P] Implement
src/parse/tasks.jsexactly per contracts/tasks-md-format.md:parseTasks(text, file)→{ phases: [{number, title, tasks}], tasks, warnings }; split on\nand strip trailing\r; skip fenced blocks (``` and~~~) and HTML comments including multi-line ones; phase regex^##\s+Phase\s+(\d+)\s*:\s*(.+?)\s*$; task regex^\s*[-*]\s+\[( |x|X)\]\s+(.*)$; ID^(T\d+)\b;[P]and[USn]markers directly after the ID in any order; dependencies/depends on\s+(T\d+(?:\s*(?:,\s*and|,|and|&)\s*T\d+)*)/i(acceptsT1, T2,T1 and T2,T1, and T2,T1 & T2); tasks before the first phase go into a synthetic phase withnumber: null, titleUnphased(W3); raise W1, W2, W3, W5, W6 (drop unknown dependency IDs), W8, W12 (repeated phase number) with the contract's exact messages and 1-based line numbers; every task line counts (M5); never throw; addtests/unit/parse-tasks.test.jscovering every table row, every warning, CRLF, fences, comments, and "total equals number of checkboxes outside code and comments" - T013 [P] Implement
src/parse/spec.js:parseSpec(text)→{ title: string | null, stories: [{label: "US<n>", title, priority}] }using^#\s+Feature Specification:\s*(.+)$(first match) and^###\s+User Story\s+(\d+)\s*[-–—]\s*(.+?)\s*\(Priority:\s*(P\d+)\)\s*$, skipping fenced blocks; addtests/unit/parse-spec.test.js(hyphen, en dash, em dash, missing priority line ignored) - T014 Implement
src/model/build-model.js(depends on T011–T013):buildModel(scanResult)→Projectper data-model.md; Featuretitlefrom spec title elsedir; Phase fieldsnumber,title,tasks,storyLabels(first-appearance order),mergedStory(set when every task carries the same single label),groups(only whenstoryLabels.length > 1; unlabeled tasks stay on the phase),key=<featureDir>/p<number or 'u'>, with@L<line>appended to every occurrence of a repeated phase number; StoryGroupkey=<phaseKey>/<label>; Taskkey=<featureDir>/<id or 'L'+line>, with@L<line>appended to every occurrence of a duplicated ID, so all keys are unique (data-model.md);Counts = { done, total, open = total - done, percent = total ? min(round(done*100/total), open ? 99 : 100) : 0 };Totals = { tasks: Counts, specs: {completed, total}, phases: {completed, total} }where a spec is completed whenhasTasks && counts.total > 0 && counts.open == 0and every feature counts towardspecs.total, and a phase is completed whencounts.total > 0 && counts.open == 0, with only phases wherecounts.total > 0counted inphases.total; Stage per the data-model table (empty,specified,planned,ready,in-progress,complete;tasks.mdwithtotal == 0staysplanned/specified/empty); W4story label USn has no matching user story in spec.md; build artifact lists withsortFeatureArtifacts/sortAssessmentArtifacts; addtests/unit/build-model.test.jsincluding 40/65 → 62 %, 199/200 → 99 %, merged vs grouped phases, stage for every row, a phase with no tasks excluded fromphases.total, and unique keys for duplicate task IDs and repeated phase numbers - T015 Implement
src/model/active.js(depends on T014):selectActive(project, {featureDirectory, gitBranch})→{ featureDir, phaseKey, storyLabel, nextTaskKey, source: "feature.json" | "git-branch" | "first-open" | "none" }following data-model.md "ActiveSelection" rules 1–6 verbatim (while any feature has open tasks, a candidate is skipped whenhasTasks && open == 0; when no feature has open tasks, candidate A else B stays active even if complete, with null phase/story/task, elsenone(FR-018); feature withouttasks.mdcan be active with null phase/task;storyLabelonly when the active phase has groups; next task = first open task in file order within the active group, or within the phase when it has no groups); addtests/unit/active.test.jswith one test per rule plus spec US1 AC6 both cases and AC7 both cases (all done without feature.json →none; all done with feature.json naming a feature → that feature,source: "feature.json", null phase/story/task) - T016 Implement
src/model/task-state.js(depends on T015):applyTaskStates(project)sets each task'sstatein the order completed → current (key == active.nextTaskKey) → blocked (anydependsOntask open) → future, adds W7next task Tnnn depends on open task Tmmmwhen the current task has an open dependency, and setssigon the project, every feature, phase, group, and task (short string from counts, stage/state, and the active flag, data-model.md "Change signature"); makebuildModelcallselectActiveandapplyTaskStatesso it returns a complete Project; addtests/unit/task-state.test.js - T017 [P] Implement
src/cli/args.jsper contracts/cli.md:parseCliArgs(argv)usingutil.parseArgsstrict mode with optionsserve,build,out,base,help/-h,version/-v; returns{mode: "serve" | "build" | "help" | "version", dir, out, base}or{error: string}for unknown options, both modes, no mode,--outmissing in build,--out/--basewith serve;normalizeBaseturnsrepo→/repo/, default/; export aUSAGEstring with the synopsis; addtests/unit/args.test.js - T018 Implement
src/render/layout.js(depends on T008):renderPage({ title, base, mode, project, main, version, generatedAt })returns a full HTML document:<body data-mode="serve|static" data-version="…">,<header data-region="header">with the project name (links to{base}index.html) and links to the constitution and each assessment's first artifact at{base}+ url,<link rel="stylesheet" href="{base}assets/styles.css">,<script src="{base}assets/overview.js" defer>, the givenmain, and<footer>with the version; all links absolute withbase(R10); addtests/unit/layout.test.js
Checkpoint: npm test passes; buildModel(scan(fakeReader)) yields a complete Project for any fixture
06 Phase 3: User Story 1 - See where the project stands at a glance (Priority: P1) 🎯 MVP
Goal: speckit-eye --serve <dir> prints a local address whose overview shows the header, overall bar and counters, the feature tree with only the active chain expanded, and the task grid.
Independent Test: Serve tests/fixtures/projects/mixed; the overall bar reads 40 / 65 (62 %), per-feature/phase/story counts match the fixture checkboxes, only the active chain is open, the next task is shown, and the project folder is unchanged.
Fixtures for User Story 1
- T019 [P] [US1] Create fixture
tests/fixtures/projects/mixed/:.specify/memory/constitution.md;.specify/assessments/speckit-dashboard/intake.mdanddecision.md;specs/001-alpha/(spec.md with US1–US2, plan.md, tasks.md with 30/30 done);specs/002-beta/(spec.md with US1–US3, plan.md, tasks.md with 10/20 done: Phase 1 Setup unlabeled, Phase 3 all[US1](merged row), Phase 4 mixing[US2],[US3]and one unlabeled task, one open task whose description saysdepends on T0xxnaming an open task so it is blocked);specs/003-gamma/(spec.md, plan.md, tasks.md with 0/15 done);specs/004-delta/(spec.md only → stagespecified); document expected counts per feature/phase/story intests/fixtures/projects/README.md - T020 [P] [US1] Create fixtures
tests/fixtures/projects/complete/(two features, every task[x]or[X], constitution) andtests/fixtures/projects/empty/(emptyspecs/kept with a.gitkeep, plus.specify/memory/constitution.md); add their expectations totests/fixtures/projects/README.md - T021 [P] [US1] Create fixture
tests/fixtures/projects/nonstandard/specs/001-odd/with spec.md and a tasks.md that triggers W1, W2, W3, W4 ([US9]), W5, W6, W7, a duplicated task ID and a repeated## Phase 2:heading (W12), plus a checkbox inside a fenced block and inside an HTML comment (both not counted); addspecs/002-emptytasks/tasks.mdwith no task lines (W8); record expected totals and warning codes/lines intests/fixtures/projects/README.md
Implementation for User Story 1
- T022 [US1] Implement the progress section in
src/render/overview.js:renderOverview(project, {base})returns the<main>inner HTML beginning with<section data-region="progress">containing the overall bar as<progress data-key="project" data-sig value="<done>" max="<total>">(no inlinestyleattribute anywhere, since the serve-mode CSP blocks them; contracts/routes.md) with the text "done / total tasks (pct %)", three counters[data-counter="specs|phases|tasks"]each "completed / total", a "Next: T012 · description" line foractive.nextTaskKey(FR-017), an "All tasks are complete" message whentotals.tasks.total > 0 && totals.tasks.open == 0, whether or not a feature is active (FR-018), and a short explanation instead of an empty page when there are no features (edge case "Empty project"); addtests/unit/overview.test.js - T023 [US1] Add the feature tree to
src/render/overview.js(<div data-region="tree">): one<details data-key data-sig data-status="done|started|not-started" [data-active]>per feature in folder order with<summary>= title · stage label · "N open / M" (FR-019a);openonly on the active feature, active phase, and active group (FR-016); completed items are neveropen, except a completed active feature when all tasks are done (FR-018), which getsopenanddata-activewhile its phases stay closed; phases as nested<details>titled "Phase N · USn – Title (Pn)" whenmergedStoryis set, otherwise "Phase N: Title" with a group sub-level per story and unlabeled tasks directly under the phase (FR-015a); tasks as<li data-key data-state>with ID, description, and a "next" mark on the current task; a warning block on the feature listingfile:line message(FR-019); extendtests/unit/overview.test.js(including the completed active feature:data-status="done",data-active,open, no open phase, no "next" mark) - T024 [US1] Add the task grid to
src/render/overview.js(<div data-region="grid">): one<a tabindex="0" data-key data-state="completed|current|blocked|future" data-parents="<featureDir> <phaseKey> [groupKey]" title="T012 · description — feature › phase">per task in tree order, nohrefuntil the click decision in T034 (FR-015b); extendtests/unit/overview.test.js - T025 [US1] Style the overview in
src/styles/input.css: the<progress>bar (track and value via::-webkit-progress-bar,::-webkit-progress-value,::-moz-progress-bar); tree status colors from the--status-*tokens (donegreen with a ✓ mark,[data-active]highlighted and combinable withdone,startedblue,not-starteddark grey);detailsopen/close animation undermotion-safe; grid of small squares colored bydata-stateusing the same tokens; two-column layout from themdbreakpoint, stacked tree-first below it (FR-015, FR-035); a[data-highlight]style for tree items highlighted from the grid - T026 [P] [US1] Implement
src/client/overview.js: exportparentKeys(dataParents)andattachHover(doc), which registersmouseover/focusinon[data-region="grid"]to setdata-highlighton the tree elements whosedata-keyis listed in the target cell'sdata-parents, and clears them onmouseout/focusout; the file ends with the one-line bootstrapif (typeof document !== "undefined") attachHover(document);; addtests/unit/client-overview.test.jscoveringparentKeysandattachHoverwith small fake element/document objects (addEventListener,querySelector,setAttribute,removeAttribute) - T027 [US1] Implement
src/render/site.js:renderSite(project, { base, mode, version, generatedAt, assets })→Map<path, {type, body}>containingindex.html(layout + overview) andassets/styles.cssandassets/overview.jsfrom the passed-inassetsstrings (site.js stays pure; callers read the files); this is the only place that decides which pages exist (FR-031, §III); addtests/unit/site.test.js - T028 [US1] Implement the server in two modules.
src/serve/handler.js:createHandler({ getSite })returns a pure(req, res)function: map/toindex.html, strip the leading/, look the path up ingetSite(); unknown paths (including any containing..or encoded variants) → 404 without touching the file system (FR-007); only GET and HEAD (others → 405); headersContent-Typeby extension,Cache-Control: no-store,X-Content-Type-Options: nosniff,Content-Security-Policy: default-src 'self'; style-src 'self'; script-src 'self'; img-src 'self' data:; addtests/unit/handler.test.jswith fakereq/resobjects (routes, 404 cases, 405, HEAD without body, every header).src/serve/server.js:startServer({ handler, host: "127.0.0.1", port: 4747, createServer = http.createServer })only listens: bind tohost, onEADDRINUSEretry once with port 0, resolve with the actual{ url, close }; addtests/unit/server.test.jswith a fakecreateServerwhoselistenemitsEADDRINUSEor succeeds (no socket is opened, §IV) - T029 [US1] Implement serve mode in
src/cli/main.js:run(argv, { stdout, stderr, createReader, startServer, readAsset, onSignal })with real defaults; handlehelp(usage to stdout, 0),version(frompackage.jsonviaimport pkg from "../../package.json" with { type: "json" }, 0), usage errors (usage to stderr, 2), missing dir or not a Spec Kit project (message naming the expectedspecs/or.specify/, 2); then scan → buildModel → renderSite(modeserve, base/) →createHandler→startServer; printspeckit-eye <version> — serving <abs dir>,Local: <url>,Watching specs/ and .specify/ for changes (Ctrl+C to stop)and each warning to stderr aswarning: <file>:<line> <message>; exit 0 on SIGINT/SIGTERM, 1 on unexpected errors (contracts/cli.md); addtests/unit/main.test.jswith injected fakes - T030 [US1] Create
bin/speckit-eye.jswith#!/usr/bin/env node, importingrunfrom../src/cli/main.jsand settingprocess.exitCodefrom its result; mark it executable (chmod +x, andgit update-index --chmod=+x) - T031 [US1] Create
tests/e2e/helpers.js:copyFixture(name)copiestests/fixtures/projects/<name>to a freshos.tmpdir()folder;startServe(dir)spawnsnode bin/speckit-eye.js --serve <dir>, resolves{ url, stdout, stderr, stop() }once aLocal:line appears (15 s timeout), and fails at once withE2E tests need port 4747 freewhen that line shows another port (the server fell back because 4747 was busy);runCli(args)resolves{ code, stdout, stderr };hashTree(dir)returns a digest of every file path + content for FR-006 checks - T032 [US1] Write
tests/e2e/us1-overview.spec.jscovering spec US1 AC1–AC10 one test each (AC1 also asserts the bar'svalue/maxare 40/65; AC6 writes.specify/feature.jsoninto the temp copy; AC7 usescompletetwice: withoutfeature.jsonnothing is active, and withfeature.jsonnaming its second feature that feature isopen,data-active,data-status="done", no phase is open, no "Next" line, and the complete message is shown; AC9 hovers a grid square and asserts thetitleanddata-highlighton its feature and phase), plusUS1 SC-001(at a 1280×720 viewport the progress section, the active feature's summary, and the "Next" line are inside the viewport without scrolling),US1 FR-015c(the dependent task inmixedhasdata-state="blocked"in the grid and the tree),US1 FR-036(a context withreducedMotion: "reduce": computedtransition-durationandanimation-nameof the bar and adetailselement are0s/none),US1 FR-038(every request made while loading and using the overview goes to the server's own origin, and nosecuritypolicyviolationevent fires),US1 FR-013(nonstandard fixture: server keeps running, warnings shown on the feature and on stderr, totals equal checkbox counts),US1 FR-015(narrow viewport 375 px stacks tree above grid), empty project (0 % with explanation),FR-005(missing dir and non-Spec-Kit dir exit 2, no args exit 2,--helpexit 0), andFR-006(hashTreeunchanged after the run) - T033 [US1] Create
docs/usage.md(serve mode:npx speckit-eye --serve <dir>, what the overview shows, stage labels, status colors, active-item rules FR-014 in plain words, link to contracts/cli.md for options/exit codes) andREADME.md(what it is,npx speckit-eye --serve .example, prerequisites Node ≥ 22, links todocs/andDEVELOPMENT.md; no developer content, §VIII); add "Added: serve mode with overview" toCHANGELOG.md - T034 [US1] Visual review (owner gate — needs a human decision): run
node bin/speckit-eye.js --serve tests/fixtures/projects/mixedand against this repository, present screenshots, ask the owner (a) what clicking a tree item or grid square does and (b) the threshold and fallback for very large grids; record the decisions inspecs/001-speckit-eye-dashboard/spec.mdunder Assumptions "Open design items" and append implementation tasks for them to this file after T070
Checkpoint: US1 is a usable MVP: npm test and npx playwright test tests/e2e/us1-overview.spec.js pass
07 Phase 4: User Story 2 - Follow progress live during a long run (Priority: P2)
Goal: In serve mode, every open page updates within ~2 s of a Spec Kit Markdown file settling, keeping scroll and expanded items, highlighting changed items, and showing a banner while disconnected.
Independent Test: Serve a temp copy of mixed, expand a non-active feature and scroll, tick a task on disk; within 2 s the counts update, scroll and expansion are unchanged, and the changed items carry data-changed.
Implementation for User Story 2
- T035 [P] [US2] Implement
src/serve/watcher.js:createWatcher({ root, gitDir, watch = fs.watch, exists, setTimeout, clearTimeout, onChange, onError })watchingspecs/,.specify/memory/,.specify/assessments/with{ recursive: true }; the single-file inputs through their parent folders without recursion, filtered by file name (.specify/→feature.json;gitDir→HEAD), because rename-saves make a watch on the file itself go silent (R2); and the project root and.specify/without recursion so that a recursive root created after startup (specs,memory,assessments) is added on the next event (refreshRoots()is idempotent and uses the injectedexists); missing roots are skipped until then; any relevant event triggers a debouncedonChange()with a 100 ms quiet period and a 500 ms maximum wait during bursts (R2); watchererrorevents go toonErrorand do not crash;close()stops all watchers and timers; addtests/unit/watcher.test.jswith a fakewatch, a fakeexists, andnode:testmock timers (single event, burst capped at 500 ms,HEADreplaced by rename still fires, unrelated file in.git/ignored,specs/created after start gets watched, close) - T036 [P] [US2] Implement
src/serve/events.js:createEventHub({ setInterval, clearInterval })withadd(req, res)(writesContent-Type: text/event-stream,Cache-Control: no-store, thenevent: hello\ndata: {"version": <n>}\n\n, removes the client onclose),broadcast(version)(writesevent: change\ndata: {"version": <n>}\n\nto every client), a: pingcomment every 25 s, andclose(); addtests/unit/events.test.jswith fake response objects - T037 [US2] Add
GET /__eventstosrc/serve/handler.js(createHandler({ getSite, events }), routed toevents.add), exempt from the site-map lookup; extendtests/unit/handler.test.js - T038 [US2] In
src/render/layout.jsadd, only whenmode == "serve",<script src="{base}assets/live.js" defer>and<div data-region="live-status" hidden>Live updates paused — reconnecting…</div>; insrc/render/site.jsaddassets/live.jsto the map only in serve mode; extendtests/unit/layout.test.jsandtests/unit/site.test.jsasserting both are absent in static mode - T039 [US2] Implement
src/client/live.jsper contracts/routes.md "Live-update protocol" ascreateLiveClient({ document, window, EventSource, fetch, DOMParser, setTimeout, requestAnimationFrame })withstart(): track the viewer's own toggles (toggleevents ondetails[data-key]→Map<key, open>); onchange: recordscrollY,data-sigperdata-key, and<progress>values,fetch(location.pathname), parse withDOMParser, replace<main>, re-apply the viewer's toggles, restorescrollY, setdata-changedfor 1.5 s on elements whosedata-sigdiffers, animate each<progress>valuefrom old to new withrequestAnimationFrame(skipped whenmatchMedia("(prefers-reduced-motion: reduce)")matches); on 404 show a notice linking to the overview; onEventSourceerrorunhide[data-region="live-status"], on the nexthellohide it and fetch once (FR-030); also export the helperscollectSigs(elements),changedKeys(oldSigs, newSigs),applyToggles(detailsList, toggles); the file ends with the one-line bootstrapif (typeof document !== "undefined") createLiveClient({ document, window, EventSource, fetch, DOMParser, setTimeout, requestAnimationFrame }).start();; addtests/unit/client-live.test.jscovering the helpers andcreateLiveClientwith fake document/window/EventSource/fetch/DOMParser objects and mock timers (swap keeps toggles and scroll,data-changedset and cleared, value animation and its reduced-motion skip, 404 notice, banner onerrorand hidden plus one fetch onhello) - T040 [US2] Wire live updates in
src/cli/main.js: create the event hub and the watcher (passinggitDirfrom the scan result, and re-creating the watcher when a rescan reports a differentgitDir); on change rescan → buildModel → renderSite; if any page body changed, increment the model version andbroadcast(version); printupdated (<n> features, <done>/<total> tasks)per rescan and each new or changed warning once; a failing rescan logs to stderr and keeps the last site; close watcher and hub on SIGINT/SIGTERM; extendtests/unit/main.test.jswith fake watcher/hub - T041 [US2] Add live-update styles to
src/styles/input.css:[data-changed]highlight animation (~1.5 s,motion-safeonly),[data-region="live-status"]fixed banner, and the 404 notice - T042 [US2] Write
tests/e2e/us2-live.spec.jscovering spec US2 AC1–AC7 one test each, with ≤ 2 s assertions: AC1 tick a task withfs.writeFile, then repeat 20 single ticks one after another and assert at least 19 appear within 2 s (US2 SC-002, 95th percentile); AC2 expand a non-active feature, scroll, tick, assertscrollYandopenunchanged; AC3 assertdata-changedon the changed task/phase/feature and the bar's newvalue; AC4 artifact page update — marktest.fixmeuntil US3 is done; AC5 createspecs/005-new/spec.md, thenUS2 FR-026delete that folder and assert the feature disappears from the tree; AC6 write a temp file andrenameovertasks.md, then 10 writes in 200 ms, assert the final content; AC7 stop the CLI, assert the banner is visible, restart on the same dir (same port 4747), assert the banner hides and content catches up - T043 [US2] Cross-platform watcher check (plan risk R2): confirm
us2-live.spec.jspasses in the CI e2e matrix on ubuntu, macOS, and Windows (T006), and run it once under WSL with the project on the Linux file system; record the results and date under R2 inspecs/001-speckit-eye-dashboard/research.md; if any platform fails, stop and request a plan amendment for thechokidarfallback instead of adding it - T044 [US2] Add a "Live updates" section to
docs/usage.md(what triggers an update, ~2 s latency, banner meaning, WSL note: keep the project on the Linux file system, files under/mnt/cedited from Windows do not raise events); add "Added: live updates in serve mode" toCHANGELOG.md
Checkpoint: US1 and US2 pass their E2E suites independently
08 Phase 5: User Story 3 - Read any artifact in two steps or fewer (Priority: P2)
Goal: Every Markdown artifact in the project has a safe, well-typeset page reachable from the overview in ≤ 2 steps via the menu or feature links.
Independent Test: Serve the artifacts fixture; every artifact opens within two clicks from the overview, tables/task lists/code render, relative links go to rendered pages, and no script from a file runs.
Implementation for User Story 3
- T045 [P] [US3] Create fixture
tests/fixtures/projects/artifacts/:.specify/memory/constitution.md;.specify/assessments/idea-x/withintake.md,decision.md,notes.md;specs/001-full/withspec.md,plan.md,research.md,data-model.md,quickstart.md,tasks.md,contracts/cli.md,checklists/requirements.md(with checkboxes that must not count),decisions.md,run-log.md, andbad name.md(W11);specs/002-partial/with onlyspec.mdandplan.md;plan.mdmust contain a table, a task list, a fencedjsblock, a```mermaidblock,<script>alert(1)</script>,<img src=x onerror=alert(1)>,[spec](./spec.md),[cli](./contracts/cli.md#synopsis),[src](../../src/index.js),[x](javascript:alert(1)), and[docs](https://example.com) - T046 [P] [US3] Implement
src/render/markdown.js:createMarkdown({ artifactsBySource, base })returnsrender(source, text)usingmarkdown-itwithhtml: false,linkify: false(R3); an in-house core rule turning list items starting with[ ]/[x]/[X]into<input type="checkbox" disabled>(checked when done); link rewriting: resolve relative hrefs against the source file's folder, keep the#fragment, map known artifact sources to{base}+ url, keephttp:,https:,mailto:links, render every other relative link as plain text (FR-023); headingids from an in-house slug (lowercase, runs of non-[a-z0-9]→-, trimmed, duplicates suffixed-1,-2); fenced blocks (includingmermaid) rendered as escaped<pre><code class="language-x">; addtests/unit/markdown.test.jscovering script/img escaping,javascript:rejected, table, task list, link rewrite cases, slugs - T047 [US3] Implement
src/render/artifact.js:renderArtifact(artifact, bodyHtml, { base, feature })→<main>containing a breadcrumb (Overview › feature title › artifact title) and<article class="prose" data-region="artifact" data-key="<source path>">; addtests/unit/artifact.test.js - T048 [US3] Add the site-wide menu to
src/render/layout.js: a<details data-region="menu">in the header (works without scripts) listing Overview, Constitution, each assessment with its artifacts, and each feature with its artifacts in data-model order, plus an always-visible "Overview" link on every page (FR-022, FR-025); extendtests/unit/layout.test.js - T049 [US3] In
src/render/overview.jslist links to each present artifact (title, in data-model order) inside every feature's<details>, above its phases; no link for missing artifacts (US3 AC1, AC5); extendtests/unit/overview.test.js - T050 [US3] Extend
src/render/site.jsto add one page per artifact (constitution, every feature artifact, every assessment artifact) usingcreateMarkdownandrenderArtifact, keyed by its url; extendtests/unit/site.test.jsasserting the page set equals the artifact set and W11 names are absent - T051 [US3] Write
tests/e2e/us3-artifacts.spec.jscovering spec US3 AC1–AC6 one test each, plusUS3 SC-004 FR-022(breadth-first crawl from the overview following at most 2 link clicks reaches every artifact page listed in the fixture),US3 FR-024(no dialog event fires on the plan page;<script>appears as text),US3 FR-023(the../../src/index.jslink is plain text and/src/index.jsreturns 404), andUS3 FR-020(mermaid block shown as code);US3 FR-038(every request while loading artifact pages goes to the server's own origin); then removetest.fixmefrom US2 AC4 intests/e2e/us2-live.spec.jsand make it pass, and addUS2 FR-026there: with theresearch.mdpage open, delete the file and assert the notice linking to the overview appears within 2 s - T052 [US3] Add an "Artifact pages" section to
docs/usage.md(which files are listed, order, menu, link behaviour, diagrams shown as code); add "Added: artifact pages and site menu" toCHANGELOG.md
Checkpoint: US1–US3 pass their E2E suites; header links to constitution/assessments now resolve
09 Phase 6: User Story 4 - Publish a shareable snapshot from CI (Priority: P3)
Goal: speckit-eye --build <dir> --out <folder> [--base /repo/] writes the same pages as a static site that works under a sub-path, without scripts, and without live updates.
Independent Test: Build mixed with --base /my-repo/, serve the output under /my-repo/ from a plain static server; overview and every artifact page load directly, links and styles work, expand/collapse works with JavaScript disabled, and no __events request is made.
Implementation for User Story 4
- T053 [P] [US4] Implement
src/build/build.js:writeSite({ site, out, projectRoot, fs })with injectedfs/promises-likefs; rejectoutequal toprojectRootor inside<projectRoot>/specsor<projectRoot>/.specify(usage error); ifoutexists, is not empty, and has no.speckit-eye-build→ return{ error, code: 2 }having written nothing (FR-033); if the marker exists, remove the folder's contents first; write every page (creating folders), then the.speckit-eye-buildmarker; return{ pages: <count> }; addtests/unit/build.test.jswith an in-memory fake fs (fresh folder, rebuild removes stale page, foreign folder untouched, out inside specs/) - T054 [US4] Static mode in
src/render/layout.jsandsrc/render/site.js: whenmode == "static", rendergenerated at <ISO time>fromgeneratedAtin the footer, omitlive.js, the live-status banner, and anything referencing__events, and prefix every link and asset withbase; add a unit test intests/unit/site.test.jsasserting that serve and static<main>HTML are identical for the same model with base/(FR-031) - T055 [US4] Implement build mode in
src/cli/main.js: scan → buildModel → renderSite(modestatic,base,generatedAt = new Date().toISOString()) →writeSite; printspeckit-eye <version> — building <abs dir> → <out> (base <base>),wrote <n> pages,<n> warnings (see above)when any, and the two-line public-exposure note from contracts/cli.md (FR-034); exit 0 on success, 2 for usage/marker errors, 1 when writing fails; extendtests/unit/main.test.js - T056 [US4] Add to
tests/e2e/helpers.js:runBuild(dir, out, base)andserveStatic(rootDir, mountPath)— a test-onlynode:httpstatic file server on port 0 that maps<mountPath>*to files underrootDir(directory →index.html, unknown → 404) - T057 [US4] Write
tests/e2e/us4-build.spec.jscovering spec US4 AC1–AC5 one test each: AC1 page count and exit 0 with summary; AC2 deep link/my-repo/features/001-alpha/plan.htmlloads with styles; AC3 overview matches serve mode (compare[data-region=progress], tree and grid text), footer shows generated time, no request to__eventsand every request stays on the static server's origin (US4 FR-038), expand/collapse works in a context withjavaScriptEnabled: false(US4 FR-037); AC4 see T059; AC5 rebuild after deleting an artifact leaves no stale page, and a foreign non-empty folder yields exit 2 with contents unchanged; plusUS4 FR-006(hashTreeof the project unchanged) andUS4 FR-034(stdout contains the exposure note) - T058 [US4] Create
docs/hosting.md: a GitHub Pages workflow sample (actions/checkout@v4,actions/setup-node@v4with Node 22,npx speckit-eye --build . --out _site --base /${{ github.event.repository.name }}/,actions/upload-pages-artifact@v3,actions/deploy-pages@v4,pages: writeandid-token: writepermissions), a prominent warning next to it that a hosted build makes the specs, research, constitution and assessments readable by anyone who can reach the site unless the host restricts access (FR-034), a note that.specify/feature.jsonis git-ignored and that CI checks outmain(push) or a detached HEAD (pull requests), which rarely matches a feature folder name, so the active feature usually falls back to the first feature with open tasks (R9), and a short Netlify note; add a "Publish a snapshot" section toREADME.mdwith the build command, the same warning, and a link todocs/hosting.md; add build options todocs/usage.md; add "Added: static build mode" toCHANGELOG.md - T059 [US4] Add
US4 AC4 FR-039totests/e2e/us4-build.spec.js: readdocs/hosting.md, extract thenpx speckit-eye --build …line from the YAML sample, substitutenode bin/speckit-eye.jsand a concrete base, run it against a temp copy ofmixed, and assert exit 0 and a loadableindex.htmlunder the base
Checkpoint: All four user stories pass their E2E suites independently
10 Phase 7: Polish & Cross-Cutting Concerns
Purpose: Security and scale verification, self-check, docs, and release readiness
- T060 [P] Write
tests/e2e/security.spec.js(FR-006, FR-007, FR-024, SC-008): servingmixed, raw HTTP requests (vianode:http, not the browser, so paths are not normalized) for/../package.json,/%2e%2e/package.json,/specs/001-alpha/spec.md,/.specify/memory/constitution.md,/.env,/src/cli/main.js,/features/../../package.jsonall return 404; every page response has the CSP andnosniffheaders, and nosecuritypolicyviolationevent fires on the overview or any artifact page; the server is not reachable on a non-loopback interface address fromos.networkInterfaces()(skip when none exists);hashTreeof the project is unchanged after serve, a live update, and a build - T061 [P] Create
tests/fixtures/generate-large.js(node tests/fixtures/generate-large.js <dir>writes 50 features × 40 tasks with spec.md, plan.md, tasks.md in 4 phases, about half the tasks checked) andtests/e2e/scale.spec.js(SC-010): overviewloadevent ≤ 2 s after navigation, a checkbox change visible ≤ 2 s,--buildcompletes ≤ 30 s - T062 [P] Write
tests/e2e/self-counts.spec.js(SC-005): build this repository's own project into a temp folder and assert, for eachspecs/*/tasks.md, that the feature's "open / total" in the built overview equals the checkbox counts computed in the test by the contract's task regex outside fenced blocks and HTML comments - T063 [P] Create
docs/architecture.md: module list by folder with one-line responsibilities, the one-way data flowproject → parse → model → render → serve/build, whyrender/site.jsis the single page decider, the live-update sequence (watcher → rescan → version → SSE → client swap), and links to contracts/ and data-model.md instead of copying them (§III, §VII) - T064 Complete
README.md: add a screenshotdocs/screenshot.pngcaptured with Playwright from themixedfixture, a feature list, both commands, and links todocs/usage.md,docs/hosting.md,docs/architecture.md,DEVELOPMENT.md, andCHANGELOG.md; keep developer content out (§VIII) - T065 Run
npm run test:coverageand add unit tests to the correspondingtests/unit/*.test.jsfiles for any uncovered branch insrc/; the only lines allowed to stay uncovered are the one-line bootstraps at the end ofsrc/client/*.js(§IV) - T066 Verify packaging: run
npm pack --dry-runand confirm the tarball containsbin/,src/withoutsrc/styles/,dist/styles.css,README.md,CHANGELOG.md,LICENSE, and nothing else; install the tarball in a temp folder and runnpx speckit-eye --serve <path to mixed fixture>to confirm it starts with one runtime dependency tree (FR-001, SC-007); record the result inDEVELOPMENT.mdrelease checklist - T067 Run every scenario in
specs/001-speckit-eye-dashboard/quickstart.md(1–6) by hand and fix any mismatch between the quickstart, the docs, and the behaviour - T068 Grep
src/for unused exports, dead code, and TODOs and remove them; confirmpackage.jsondependencieslists onlymarkdown-it(§I, §II) - T069 Complete
DEVELOPMENT.md: "Project structure" (short tree ofsrc/,tests/,docs/linking todocs/architecture.md) and "Release process" (updateCHANGELOG.md[Unreleased]→[X.Y.Z] - date, bumpversioninpackage.jsononly,npm pack --dry-runcheck from T066,git tag vX.Y.Z,npm publish) (§VI, §IX) - T070 Prepare the first release: move the
[Unreleased]entries inCHANGELOG.mdto## [1.0.0] - <date>, set"version": "1.0.0"inpackage.json; leave tagging andnpm publishto the owner (plan §VI)
Owner decisions from the T034 visual review (spec Assumptions "Open design items")
These tasks do not depend on T070 and can run any time after US1. Do them before T065–T070 so coverage, docs and the release include them.
- T071 [US1] Grid click in
src/render/overview.js: give each tree task<li>anidbuilt from itsdata-key(a stable, attribute-safe form such astask-+ the key with every character outside[A-Za-z0-9_-]replaced by-, with a numeric suffix when two keys collide) and give each grid squarehref="#<that id>", so a click jumps to the task with or without JavaScript (browsers open closed<details>ancestors of a fragment target); update the doc comment that says "Nohrefuntil … T034"; extendtests/unit/overview.test.js(every square'shrefmatches exactly one treeid, ids are unique for repeated task IDs) - T072 [US1] Grid click in
src/client/overview.js: exportattachClick(doc)(and call it from the bootstrap line next toattachHover), which onclickof a grid square prevents the default jump, setsopenon the feature, phase and group<details>listed in itsdata-parents, then callsscrollIntoView({ block: "center" })on the target task (behaviorsmoothunlessmatchMedia("(prefers-reduced-motion: reduce)")matches, FR-036) and marks it withdata-highlightuntil the next click; tree items keep their native toggle and get no click handler; extendtests/unit/client-overview.test.jswith fakes (parents opened, target scrolled, default prevented, reduced-motion usesauto) - T073 [US1] Large-grid fallback in
src/render/overview.jsandsrc/styles/input.css: whentotals.tasks.total≤ 1,000 keep the single grid; above 1,000 renderdata-layout="rows"with one row per feature (a feature label, then its squares, smaller via a[data-layout="rows"]style); above 5,000 renderdata-layout="bars"with one<progress data-key="<featureDir>" value max>bar per feature labelled "title · done / total", linking to the feature's tree item; keepdata-key,data-state,data-parents,titleandhrefon squares in both square layouts so hover (T026), click (T072) and live updates still work; export the thresholds as named constants; extendtests/unit/overview.test.js(1,000 → single grid, 1,001 → rows, 5,001 → bars) and add the new styles with no inlinestyleattribute (CSP) - T074 [US1] Add E2E tests to
tests/e2e/us1-overview.spec.js:US1 FR-015b click(inmixed, with the task's phase collapsed, clicking a grid square opens its feature, phase and story and the task is inside the viewport; the same withjavaScriptEnabled: falsereaches the task through the fragment, FR-037) andUS1 FR-015b large grid(usingtests/fixtures/generate-large.jsfrom T061 with 2,000 tasks the grid hasdata-layout="rows"and one row per feature); updatedocs/usage.md(what a grid click does, how large grids are shown) and add "Added: click a grid square to jump to its task; compact grid for large projects" toCHANGELOG.md - T075 [US2] Add a
wsljob to.github/workflows/ci.yml: on windows-latest, useVampire/setup-wsl(Ubuntu 24.04, WSL 2 where the runner supports it), copy the checkout into the Linux file system, install Node 22 and pnpm, and runtests/e2e/us2-live.spec.js, so the WSL part of T043 runs on every push instead of by hand; record the WSL version the runner provides under R2 inresearch.md
11 Dependencies & Execution Order
Phase Dependencies
- Setup (Phase 1): no dependencies
- Foundational (Phase 2): depends on Setup (T001 for
markdown-it/node:testscripts). BLOCKS all stories - US1 (Phase 3): depends on Foundational. MVP
- US2 (Phase 4): depends on US1 (server, CLI serve mode, overview DOM contract, E2E helpers)
- US3 (Phase 5): depends on US1 (server, site.js, layout, E2E helpers). Independent of US2 except T051 un-fixme-ing US2 AC4
- US4 (Phase 6): depends on US1; its artifact deep-link checks need US3's artifact pages. Independent of US2 (live parts are simply omitted)
- Polish (Phase 7): depends on all stories
User Story Dependency Graph
Setup → Foundational → US1 ─┬─→ US2 ─────────────┐
├─→ US3 ─┬───────────┼─→ Polish
│ └─→ US4 ────┘
└─→ (T034 visual review: owner gate, can run alongside US2/US3)
Within Foundational
- T008, T009, T010, T012, T013, T017 are independent
- T011 needs T009 + T010; T014 needs T011–T013; T015 needs T014; T016 needs T015; T018 needs T008
Within Each Story
- Fixtures before E2E specs; pure modules before
site.js;site.js+server.jsbeforemain.js;main.js+binbefore E2E specs - Tasks editing the same file (
overview.jsT022→T023→T024→T049;layout.jsT018→T038→T048→T054;site.jsT027→T038→T050→T054;main.jsT029→T040→T055;handler.jsT028→T037) run in that order
Parallel Opportunities
- Setup: T002–T007 in parallel after T001
- Foundational: T008, T009, T010, T012, T013, T017 in parallel
- US1: fixtures T019–T021 and T026 in parallel with T022–T025
- US2: T035 and T036 in parallel
- US3: T045 and T046 in parallel; US3 can run alongside US2 once US1 is done (different files except
layout.js/site.js, so sequence T038 and T048/T050) - Polish: T060–T063 in parallel
12 Parallel Example: Foundational
Task: "Implement src/render/html.js with tests/unit/html.test.js" (T008)
Task: "Implement src/project/reader.js + tests/unit/fake-reader.js" (T009)
Task: "Implement src/project/artifacts.js with tests/unit/artifacts.test.js" (T010)
Task: "Implement src/parse/tasks.js with tests/unit/parse-tasks.test.js" (T012)
Task: "Implement src/parse/spec.js with tests/unit/parse-spec.test.js" (T013)
Task: "Implement src/cli/args.js with tests/unit/args.test.js" (T017)
13 Parallel Example: User Story 1
Task: "Create fixture tests/fixtures/projects/mixed/" (T019)
Task: "Create fixtures complete/ and empty/" (T020)
Task: "Create fixture nonstandard/" (T021)
Task: "Implement src/client/overview.js hover highlight" (T026)
14 Parallel Example: User Story 2
Task: "Implement src/serve/watcher.js with fake fs.watch + mock timers" (T035)
Task: "Implement src/serve/events.js SSE hub" (T036)
15 Implementation Strategy
MVP First (User Story 1 Only)
- Phase 1 Setup → Phase 2 Foundational
- Phase 3 US1 →
npm test+us1-overview.spec.jsgreen - STOP and VALIDATE with quickstart Scenario 1; run T034 visual review with the owner
Incremental Delivery
- Setup + Foundational → the model is correct and unit-tested
- US1 → local overview (MVP)
- US2 → live updates (the watcher cross-platform check T043 is the main risk; resolve before moving on)
- US3 → artifact pages (can overlap with US2)
- US4 → static hosting
- Polish → security, scale, docs, release prep
16 Notes
- [P] = different files, no dependency on an unfinished task
- Every E2E test name starts with its
USnand/orFR-xxx/SC-xxxids (§V) - A task is done only when its unit tests (or E2E spec) pass and
CHANGELOG.md/docs are updated where it says so - T034 and T043 may stop the run for an owner decision or a plan amendment; do not work around them
- Commit after each task or logical group