speckit-eye Home

speckit-eye

Project overview

98 %208 of 212 tasks
Features1 / 32 in progress
Phases22 / 242 remaining
Open tasks4across 2 features
Up next T080 Run the full E2E suite in `chromium`, `chromium-nojs` and `chromium-mobile` locally and confirm CI passes in `firefox` and `webkit` (SC-016); fix engine-specific issues (for example the exclusive `<details name>` fallback in `src/client/feature.js`, `line-clamp` fallback, clipboard permission differences in tests) Dashboard Redesign — Sidebar Shell, Task Map, Feature Pages, Document Reader and Themes › Phase 9 · Polish & Cross-Cutting Concerns Open quickstartView task

Features

  • 003First npm Release, Safe Release Automation, and Documentation Site with Live Project Dashboard3 open48/51
    • Phase 1Setup (Shared Infrastructure)4/4Complete
      • T001Update `package.json`: add `"repository": {"type": "git", "url": "git+https://github.com/SerafAC/speckit-eye.git"}`, `"homepage": "https://serafac.github.io/speckit-eye/"`, `"bugs": {"url": "https://github.com/SerafAC/speckit-eye/issues"}`, `"author": "SerafAC"`, `"keywords": ["spec-kit", "speckit", "github-spec-kit", "spec-driven-development", "dashboard", "progress", "tasks", "cli", "static-site"]`, `"publishConfig": {"access": "public", "provenance": true}`; add script `"prepublishOnly": "node -e \"if(!process.env.GITHUB_ACTIONS){console.error('Publish only from the Release workflow: see docs/releasing.md');process.exit(1)}\""` (research R8); keep `version` `1.0.0`, `files`, `engines` unchanged
      • T002Add dev dependency `@docmd/core` pinned exactly (`"0.9.7"`, no caret) with `pnpm add -D -E @docmd/core@0.9.7`; in `pnpm-workspace.yaml` add `esbuild: false` and `'@docmd/engine-rust': false` under `allowBuilds` with a comment that docmd falls back to its JS engine (research R2); commit the updated `pnpm-lock.yaml`; verify `pnpm exec docmd --version` prints `0.9.7`
      • T003Add `site/` and `*.tgz` to `.gitignore`
      • T004Update `playwright.config.js`: add a project `tooling` (Desktop Chrome, viewport 1440 × 900) with `testMatch: /(release-.*|site|repo-health)\.spec\.js/`; add the same pattern to `DESKTOP_IGNORE` so `chromium`, `firefox`, `webkit` do not run the 003 suites; update the header comment (the tooling suites test packaging, scripts and the docs site, not browser engines, research R14). The project runs wherever `pnpm run test:e2e` runs, i.e. in the CI E2E job on Linux, macOS and Windows; this is intended (the package and the docs build must work on every supported OS). If a suite proves OS-specific, fix it rather than skipping an OS
    • Phase 2Foundational (Blocking Prerequisites)2/2Complete
      • T005Create `scripts/release/release.js` with the command dispatcher only: `main(argv, io)` parses `<command>` and options with `util.parseArgs` (strict), dispatches to `bump`, `verify`, `notes`, `release-commit`, `pack-check` (each a stub returning exit 1 "not implemented" until its story task), prints usage and exits 2 on an unknown command or bad options; exports `parseVersion(text)` → `{major, minor, patch, pre: {id, n} | null}` or `null`, accepting only `MAJOR.MINOR.PATCH` with optional `-<id>.<n>` (no build metadata, data-model Version), and `formatVersion(v)`; one-line entry point at the end. Unit tests for dispatch, usage/exit codes and `parseVersion`/`formatVersion` (valid, `1.0`, `01.0.0`, `1.0.0+build`, `1.0.0-rc`, `1.0.0-rc.1`) in `tests/unit/release.test.js`
      • T006Update `.github/workflows/ci.yml` (contracts/workflows.md "ci.yml"): add triggers `workflow_call` and `workflow_dispatch`; add `permissions: {}` at the top and `permissions: contents: read` per job; pin `actions/checkout`, `pnpm/action-setup`, `actions/setup-node`, `actions/upload-artifact`, `Vampire/setup-wsl` to full commit SHAs with version comments; add `persist-credentials: false` to every checkout; in the `e2e` job's test command keep `pnpm run test:e2e` (the `tooling` project runs with it). Keep the job names, matrices and the macOS App Nap step unchanged
    • Phase 3User Story 1 - Install speckit-eye from npm (Priority: P1) 🎯 MVPP13/3Complete
      • T007Implement `pack-check <file-list.json>` in `scripts/release/release.js` (contracts/release-cli.md): export pure `checkPackFiles(paths)` → `{unexpected: string[], missing: string[]}` where allowed = `package.json`, `README.md`, `CHANGELOG.md`, `LICENSE`, `bin/speckit-eye.js`, `src/**/*.js` except `src/styles/**`, `dist/styles.css`, `dist/fonts/*.woff2`, `dist/fonts/OFL-*.txt`, and required = each fixed file plus at least one `src/` file and at least one `.woff2`; `main` reads the JSON (accepting both the `npm pack --dry-run --json` array form `[{files:[{path}]}]` and the `pnpm pack --json` object form `{files:[{path}]}`), prints each unexpected and missing path to stderr and exits 1 if any, else 0. Unit tests (exact allowed set passes; `tests/…`, `specs/…`, `src/styles/input.css`, `docs/x.md`, `.github/…` rejected; missing `LICENSE` and no `.woff2` reported; both JSON forms) in `tests/unit/release.test.js`
      • T008Create E2E suite `tests/e2e/release-package.spec.js` (tooling project): in a temp folder run `pnpm pack --json --pack-destination <tmp>` from the repo root, run `node scripts/release/release.js pack-check` on its output (exit 0) — `003 US1 FR-002 the package contains only runtime files, README, CHANGELOG and LICENSE`; check the packed `package.json` has `description`, `license`, `repository`, `homepage`, `bugs`, `keywords`, `engines` — `003 US1 FR-001 the package carries complete metadata`; `npm install <tgz>` into an empty temp folder, spawn `npx speckit-eye --serve <copy of tests/fixtures/projects/mixed>` from there, wait for the `Local:` line, open it in the page and expect `40 / 65 tasks (62 %)` — `003 US1 FR-001 the installed tarball serves a project's overview`; stop the server; reuse helpers from `tests/e2e/helpers.js` (fixture copy, spawn, port 4747)
      • T009In `DEVELOPMENT.md`'s release checklist, replace the manual "Package contents" and "Installed tarball runs" items with one line saying both are now automated by `release.js pack-check` and `tests/e2e/release-package.spec.js` (the release section is rewritten in T019)
    • Phase 4User Story 2 - Cut a release safely by following one guide (Priority: P1)P111/11Complete
      • T010Implement pure `nextVersion(current, kind, preid = "rc")` in `scripts/release/release.js` following the data-model "Next version" table exactly (kinds `patch`, `minor`, `major`, `prepatch`, `preminor`, `premajor`, `prerelease`; from a pre-release, `patch`/`minor`/`major` finish it when the base already matches, e.g. `1.3.0-rc.1` + `patch` → `1.3.0`; `prerelease` from a stable version behaves as `prepatch`; `prerelease` with the same id increments `n`, with a different id starts at `0`); throws on an unknown kind. Unit tests covering every cell of the table plus `1.0.0-beta.2` + `prerelease --preid rc` → `1.0.0-rc.0` in `tests/unit/release.test.js`
      • T011Implement changelog functions in `scripts/release/release.js` (data-model "Changelog section"): `parseChangelog(text)` → `{head, unreleased: {body}, sections: [{version, date, body}], links}` (throws `malformed changelog` without `# Changelog` or `## [Unreleased]`); `hasEntries(body)` (at least one line starting `- `); `releaseChangelog(text, {version, date, repoUrl})` → new text with an empty `## [Unreleased]` followed by `## [X.Y.Z] - YYYY-MM-DD` holding the former Unreleased body (sub-headings kept), then the older sections unchanged, then rewritten link definitions: `[Unreleased]: <repo>/compare/vX.Y.Z...HEAD`, and per released version `[V]: <repo>/compare/v<previous>...vV` or, for the oldest, `[V]: <repo>/releases/tag/vV`; `sectionNotes(text, version)` → the section body trimmed, or throws; `repoUrlFrom(pkg)` → `repository.url` without `git+` and `.git`. Unit tests with sample changelogs (first release from the real shape of `CHANGELOG.md`, second release, sub-headings preserved, empty Unreleased, malformed input, notes of a missing version) in `tests/unit/release.test.js`
      • T012Implement the `bump` command in `scripts/release/release.js` (contracts/release-cli.md): `bump <kind> [--preid rc]` or `bump --version X.Y.Z` (exactly one); reads `package.json` and `CHANGELOG.md`; refuses (exit 1, nothing written, stderr message) when Unreleased has no entries (FR-008), when the version is invalid, or when the changelog already has a `## [X.Y.Z]` section; `--version` equal to the current version is allowed (first release `1.0.0`); writes `package.json` with only `version` changed (`JSON.stringify(pkg, null, 2) + "\n"`, key order kept) and the released changelog with today's UTC date from `io.now()`; prints the new version. Unit tests with fake I/O (writes both files, first-release `--version 1.0.0`, empty Unreleased writes nothing, duplicate section refused, both `<kind>` and `--version` rejected) in `tests/unit/release.test.js`
      • T013Implement `verify --tag vX.Y.Z` and `notes --version X.Y.Z` in `scripts/release/release.js` (contracts/release-cli.md): `verifyRelease({tag, pkg, changelog})` → `{version, tag, prerelease, distTag}` or throws with the failed check named (`tag v1.2.0 does not match package.json version 1.1.0`, `newest changelog section is 1.1.0, not 1.2.0`, `changelog section 1.2.0 has no entries`, `invalid tag`); `distTag` = `next` when the version contains `-`, else `latest` (FR-016); `verify` prints `version=`, `tag=`, `prerelease=`, `dist-tag=` lines; `notes` prints `sectionNotes`. Unit tests for each check and both dist-tags in `tests/unit/release.test.js`
      • T014Implement `release-commit --version X.Y.Z --head-ref <ref> --tag-exists <true|false>` in `scripts/release/release.js`: pure `releaseCommitDecision({version, headRef, tagExists, changelog})` → `{release: true}` only when `headRef === "release/next"`, `tagExists === false` and the changelog has `## [X.Y.Z]`; otherwise `{release: false, reason}` (`not merged from release/next`, `tag vX.Y.Z already exists`, `no changelog section for X.Y.Z`); prints `release=` and `reason=` lines, always exit 0 (research R10, edge case "hand-edited version"). Unit tests for all four outcomes in `tests/unit/release.test.js`
      • T015Create `.github/workflows/bump.yml` (contracts/workflows.md "bump.yml"): `workflow_dispatch` inputs `kind` (choice `patch`, `minor`, `major`, `prepatch`, `preminor`, `premajor`, `prerelease`, `explicit`), `version` (string), `preid` (string, default `rc`); fail unless `github.ref == 'refs/heads/main'`; `permissions: {}` then job `contents: write`, `pull-requests: write`, `actions: write`; `concurrency: bump`; steps: checkout `main`, setup pnpm/node 22, run `node scripts/release/release.js bump …` (`--version ${{ inputs.version }}` when `kind == explicit`, else `${{ inputs.kind }} --preid ${{ inputs.preid }}`), capture the version, commit `chore(release): vX.Y.Z` as `github-actions[bot]`, `git push --force origin HEAD:release/next`, write the PR body (release notes from `release.js notes` + "Merging this PR tags vX.Y.Z and starts the Release workflow, which waits for the repository owner's approval"), then `gh pr create --base main --head release/next --title "Release vX.Y.Z"` or, when a PR from `release/next` is open, `gh pr edit` it (edge case "previous bump still open"), then `gh workflow run ci.yml --ref release/next` (research R9). Pass inputs through `env:`, never interpolate `${{ inputs.* }}` directly into `run:` scripts (script injection)
      • T016Create `.github/workflows/release.yml` (contracts/workflows.md "release.yml", research R10): triggers `push` to `main` with paths `package.json`, `CHANGELOG.md`; `push` tags `v*`; `workflow_dispatch` without inputs (re-run for an existing tag; there is no dry-run mode — the rehearsal is the real run reviewed at the approval step); `permissions: {}`; `concurrency: {group: release, cancel-in-progress: false}`. Job `prepare` (`contents: read`, `pull-requests: read`): on a push to `main`, get the head ref of the PR for `github.sha` with `gh api repos/${{ github.repository }}/commits/${{ github.sha }}/pulls --jq '.[0].head.ref // ""'`, check the remote tag with `git ls-remote --tags origin`, run `release.js release-commit`; on a tag push or dispatch, require `github.ref_type == 'tag'` (dispatch on a branch fails with "run this workflow on a tag"); outputs `release`, `version`, `tag`, `sha`. Jobs `ci` (`uses: ./.github/workflows/ci.yml`, `contents: read`) and `verify` (`contents: read`; checkout with `fetch-depth: 0`; `git merge-base --is-ancestor <sha> origin/main` (FR-009); `release.js verify --tag`; fail if `npm view speckit-eye@<version> version` succeeds (FR-010); `pnpm pack --json`; `release.js pack-check`; `release.js notes > notes.md`; upload artifact `release` with the `.tgz` and `notes.md`) both only when `prepare.outputs.release == 'true'`. Job `publish` needs `ci` + `verify`, `environment: npm`, permissions `contents: write`, `id-token: write`: checkout with credentials, download the artifact, create and push the annotated tag `vX.Y.Z` on `sha` if it does not exist, setup-node 24 with `registry-url: https://registry.npmjs.org`, `npm publish ./speckit-eye-<version>.tgz --access public --provenance --tag <dist-tag>`, env `NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}` (research R7). Job `github-release` needs `publish`, `contents: write`: download the artifact and run `gh release create <tag> --verify-tag --title <tag> --notes-file notes.md` plus `--prerelease` when prerelease (FR-015). All values reach shell steps through `env:`
      • T017Create fixture files `tests/fixtures/release/package.json` (`{"name": "speckit-eye", "version": "1.0.0", "repository": {"type": "git", "url": "git+https://github.com/SerafAC/speckit-eye.git"}}`) and `tests/fixtures/release/CHANGELOG.md` (the Keep a Changelog head of the real file, an `## [Unreleased]` section with `### Added` and two entries, and the `[Unreleased]: …/commits/main` link line) — a frozen sample, never the repository's live files, so the suite keeps passing after real releases. Create E2E suite `tests/e2e/release-flow.spec.js` (tooling project): copy the two fixture files into a temp folder, run `node <repo>/scripts/release/release.js` there with `cwd` = temp. Tests: `003 US2 FR-006 bump moves Unreleased under the new dated version` (`bump --version 1.0.0` → stdout `1.0.0`, empty Unreleased, `## [1.0.0] - <today UTC>` with the former entries, link lines); `003 US2 FR-008 bump refuses an empty Unreleased section` (second run exits 1, files unchanged by hash); `003 US2 FR-010 verify accepts a matching tag and names latest`; `003 US2 FR-010 verify rejects a tag that does not match package.json` (exit 1, stderr names the mismatch, SC-003); `003 US2 FR-016 a pre-release verifies with the next dist-tag` (add an entry, `bump preminor` → `1.1.0-rc.0`, `verify` → `dist-tag=next`); `003 US2 FR-015 notes print the version's changelog section`; `003 US2 FR-002 pack-check rejects an unexpected file` (hand-made list with `tests/x.js` → exit 1); `003 US2 FR-009 release-commit ignores a version change not merged from release/next`
      • T018Write `docs/releasing.md` (FR-018): overview diagram (bump → PR → merge → Release: prepare, ci, verify, approval, tag, publish, GitHub release); **One-time setup** (claim/confirm the npm name; create the `npm` environment with the repository owner as the only required reviewer, self-review allowed, deployment refs `main` and `v*`; first-release granular npm token with write access and 1-day expiry as environment secret `NPM_TOKEN`; Settings → Actions → "Allow GitHub Actions to create and approve pull requests"; Settings → Pages → Source "GitHub Actions"; tag ruleset protecting `v*` from update and deletion; immutable releases; private vulnerability reporting); **Cutting a release**; **Cutting a pre-release** (`prepatch`/`preminor`/`premajor`/`prerelease`, `next` dist-tag, how users install it); **The first release** (`explicit` + `1.0.0`, then switch to trusted publishing: configure the trusted publisher for `SerafAC/speckit-eye`, workflow `release.yml`, environment `npm`; set "Require two-factor authentication and disallow tokens"; revoke the token; delete `NPM_TOKEN`); **Verifying a release** (`npm view`, provenance on npmjs.com, `npm audit signatures`, tag, GitHub release, changelog); **When something goes wrong** (a check failed; approval rejected; publish succeeded but the GitHub release failed → re-run failed jobs; a tag pushed by hand; re-running a release by dispatching **Release** on its tag; there is no dry-run mode — to rehearse, review the pending run at the approval step and reject it; a broken version → `npm deprecate speckit-eye@X "<reason>"` and ship a patch, never unpublish/reuse; name taken); **Why it is safe** (the checks of FR-009–FR-017 in one list)
      • T019Update `DEVELOPMENT.md` "Release process": replace the manual steps and the release checklist with a short summary (the three maintainer actions: start **Bump version**, merge the PR, approve the release) and a link to `docs/releasing.md` (FR-019); mention `scripts/release/release.js` in "Project structure" and that `npm publish` from a working copy is refused by `prepublishOnly`
      • T020Prepare `CHANGELOG.md` for the first bump: add, at the bottom, the link definition `[Unreleased]: https://github.com/SerafAC/speckit-eye/commits/main` (rewritten by `bump` on release); add a unit test in `tests/unit/release.test.js` that runs `parseChangelog` and `releaseChangelog` on an inline copy of the file's head, first entries and link line, so the real file's shape is covered without reading it
    • Phase 5User Story 3 - Read the documentation on a website (Priority: P2)P28/8Complete
      • T021Create `docmd.config.js` (research R3): `import pkg from "./package.json" with { type: "json" }`; export default `{ title: "speckit-eye", url: pkg.homepage (as-is: docmd strips the trailing slash itself, so the config holds no logic to unit-test, §IV), src: "docs", out: "site", markdown: { breaks: false, linkify: false }, navigation: [Home "/", Usage "/usage", Hosting a snapshot "/hosting", Architecture "/architecture", Releasing "/releasing", external "Contributing" → `https://github.com/SerafAC/speckit-eye/blob/main/CONTRIBUTING.md`, external "Development" → `https://github.com/SerafAC/speckit-eye/blob/main/DEVELOPMENT.md` (FR-021), external "Project status & live demo" → `${pkg.homepage}status/`, external "npm" → `https://www.npmjs.com/package/speckit-eye`, external "GitHub" → repository URL], plugins: { ai: false, analytics: false } }` — confirm the exact key names for navigation entries and external links against `node_modules/@docmd/core/dist/utils/config-schema.js` and `@docmd/parser`'s `normalizeNavPaths`
      • T022Move `docs/screenshot.png` and `docs/screenshot-dark.png` to `docs/assets/` with `git mv` (docmd copies only `docs/assets/`, research R4); update the `<picture>` sources in `README.md` to `docs/assets/screenshot-dark.png` and `docs/assets/screenshot.png`; grep the repository for other references to the old paths (tests, docs) and update them
      • T023In `docs/architecture.md` and `docs/usage.md`, replace every relative link into `specs/` (`../specs/…`) with the absolute URL `https://github.com/SerafAC/speckit-eye/blob/main/specs/…` (directories: `…/tree/main/specs/…`); in `docs/usage.md` rephrase the "Links to other documents" bullet so the example paths `./plan.md` and `contracts/cli.md#synopsis` are not written in Markdown link syntax (research R4)
      • T024Create `docs/index.md` (contracts/site.md "Home page"): front matter `title: speckit-eye`; one-paragraph description; the screenshot `![…](assets/screenshot.png)` with the README's alt text; "Install and run" with `npx speckit-eye --serve .` and Node ≥ 22; a prominent link "Project status & live demo" to `https://serafac.github.io/speckit-eye/status/` (FR-026) with one sentence explaining it is this repository's own specs shown by speckit-eye; links to the npm package and the repository; a "Guides" list linking Usage, Hosting a snapshot, Architecture, Releasing; "Project" links to `CONTRIBUTING.md`, `DEVELOPMENT.md`, `CHANGELOG.md`, `SECURITY.md` on GitHub (absolute URLs, `blob/main`). Do not copy README sections (§III)
      • T025Create `scripts/build-site.js` (contracts/site.md "Build command"), docs part: export pure `sitePaths(homepage)` → `{home: "<homepage with trailing />", base: "<URL path of homepage>", statusBase: "<base>status/", statusUrl: "<home>status/"}` (throws unless `https:`/`http:`); `main(argv, io)` removes `site/` (`io.rm`), runs `docmd build` through `io.run` (the local binary `node_modules/.bin/docmd`), and returns its exit code (the dashboard step is added in T031); add script `"docs:build": "node scripts/build-site.js"` to `package.json`. Unit tests for `sitePaths` (`https://serafac.github.io/speckit-eye/` and without trailing slash → base `/speckit-eye/`, statusBase `/speckit-eye/status/`; root domain → base `/`; `ftp:` throws) and for `main` with fake `rm`/`run` (order of calls, non-zero exit propagated) in `tests/unit/build-site.test.js`
      • T026Create `.github/workflows/pages.yml` (contracts/workflows.md "pages.yml", research R12): triggers `push` to `main` and `pull_request` with paths `docs/**`, `docmd.config.js`, `scripts/build-site.js`, `src/**`, `bin/**`, `specs/**`, `.specify/**`, `package.json`, `pnpm-lock.yaml`, `.github/workflows/pages.yml`, plus `workflow_dispatch`; `permissions: {}`; `concurrency: {group: pages, cancel-in-progress: false}`; job `build` (`contents: read`): checkout, pnpm, node 22, `pnpm install --frozen-lockfile`, `pnpm run docs:build`, `pnpm exec docmd validate`, `actions/upload-pages-artifact` with `path: site`; job `deploy` (`if: github.event_name != 'pull_request'`, needs `build`, `pages: write`, `id-token: write`, `environment: {name: github-pages, url: ${{ steps.deployment.outputs.page_url }}}`): `actions/deploy-pages`. Pin all actions by SHA
      • T027Create E2E suite `tests/e2e/site.spec.js` (tooling project), docs part: in `beforeAll` run `pnpm run docs:build` and start a small static file server (Node `http`, in `tests/e2e/helpers.js` as `serveStatic(root, prefix)`) that serves `site/` under `/speckit-eye/` on port 4747 and returns 404 elsewhere. Tests: `003 US3 FR-021 the home page links to every guide, npm, GitHub and the dashboard` (Usage, Hosting a snapshot, Architecture, Releasing reachable in ≤ 2 clicks, SC-006); `003 US3 FR-021 the navigation lists every guide plus Contributing and Development`; `003 US3 FR-023 the site has no broken internal links` (crawl every same-origin `href`/`src` under `/speckit-eye/`, excluding `/speckit-eye/status/` which the US4 part covers, and expect 200 for each); `003 US3 FR-020 a guide page shows the text of its docs/ file` (a heading from `docs/usage.md` appears on `/speckit-eye/usage/`); `003 US3 FR-023 docmd validate passes` (spawn `pnpm exec docmd validate`, exit 0)
      • T028Update `README.md` "Documentation" section: add a first bullet linking the documentation website `https://serafac.github.io/speckit-eye/` (US3 acceptance scenario 5); keep the links to the files under `docs/`, adding `docs/releasing.md`
    • Phase 6User Story 4 - See the project's own status and a live demo (Priority: P2)P28/8Complete
      • T029Add the `--home <url>` option to `src/cli/args.js` (contracts/cli-home.md): new `home` string option; `CliArgs.home` (`string | null`, `null` by default and for serve/help/version); `--home` with `--serve` → `{error: "--home can only be used with --build"}`; a value that is not a URL with protocol `http:` or `https:` → `{error: "--home must be an http or https URL"}`; update `USAGE` with ` --home <url> Link back to <url> from every page, for --build (for example your docs site)`. Unit tests (accepted https URL, rejected `javascript:alert(1)`, `/relative`, `ftp://x`; serve + home rejected; default `null`; usage text) in `tests/unit/args.test.js`
      • T030Add the Lucide `house` icon to `src/render/icons.js` (same format and ISC notice as the other icons) and a test that it renders in `tests/unit/icons.test.js`
      • T031Render the Home link (contracts/cli-home.md "Rendering"): `renderPage` in `src/render/layout.js` takes `home = null`; when set, the sidebar gets `<a href="<home>" data-part="home">${icon("house")}<span>Home</span></a>` directly after `<p data-part="project-name">`, the mobile menu gets the same link after its `<summary>`, and the document-page rail gets `<a href="<home>" data-part="home" aria-label="Home" title="Home">${icon("house")}</a>` directly after the brand; when `home` is `null` the output is unchanged. Thread `home` through `renderSite` in `src/render/site.js` (every `renderPage` call) and pass `args.home` from the build branch of `src/cli/main.js` (serve passes nothing). Add the `data-part="home"` link style to `src/styles/input.css` next to the brand style (tokens only, no inline styles). Unit tests: with `home` the three placements exist with the escaped URL; without `home` the HTML of every page type equals the output before this change (snapshot the current output in the test first) in `tests/unit/layout.test.js` and `tests/unit/site.test.js`; `main` passes `home` only in build mode in `tests/unit/main.test.js`
      • T032Extend `scripts/build-site.js`: after `docmd build`, run `node bin/speckit-eye.js --build . --out site/status --base <statusBase> --home <home>` through `io.run`; return the first non-zero exit code (FR-024: a failing dashboard build fails the whole build). Unit tests for the second call's arguments and failure propagation in `tests/unit/build-site.test.js`
      • T033Update `README.md`: directly under the intro paragraph add a line with the links "Documentation" → `https://serafac.github.io/speckit-eye/` and "Project status & live demo" → `https://serafac.github.io/speckit-eye/status/` (this repository's own specs shown by speckit-eye) (FR-026)
      • T034Document `--home`: add the option to `docs/usage.md` (build mode) and `docs/hosting.md` (options list, with an example linking back to a docs site), and add the `--home` row and synopsis to `specs/001-speckit-eye-dashboard/contracts/cli.md` as described in `contracts/cli-home.md`
      • T035Extend `tests/e2e/site.spec.js`, status part: `003 US4 FR-025 the dashboard of this repository is served at /status/` (open `/speckit-eye/status/`, overview heading and sidebar present); `003 US4 FR-025 the dashboard counts match this repository's tasks.md checkboxes` (count `- [x]`/`- [X]` and `- [ ]` task lines in `specs/*/tasks.md` the same way `tests/e2e/self-counts.spec.js` does — reuse its helper, moving it into `tests/e2e/helpers.js` if needed — and compare with the stats card, SC-007); `003 US4 FR-027 every link, style, font and script of the dashboard loads under /status/` (crawl same-origin URLs from the overview, one feature page and one document page; all 200; the search dialog opens and finds a task; the theme switch changes the theme); `003 US4 FR-027 the Home link returns to the documentation home`; `003 US4 FR-026 the docs home and README link to the dashboard` (home page link resolves to `/speckit-eye/status/`; `README.md` contains `https://serafac.github.io/speckit-eye/status/`)
      • T036Add a build-mode E2E test to `tests/e2e/us4-build.spec.js`: `003 US4 FR-027 --home adds a Home link to every page of a build` (build the `mixed` fixture with `--home https://example.com/docs/`, check overview, a feature page and a document page) and `003 US4 FR-027 a build without --home has no Home link`
    • Phase 7User Story 5 - Contribute to an open-source project with clear rules (Priority: P3)P38/8Complete
      • T037Create `CONTRIBUTING.md`: how to report a bug or propose a feature (issue templates), the Spec Kit flow used by this project (specify → clarify → plan → tasks → implement, `specs/`), the constitution's Quality Gates in short with a link to `.specify/memory/constitution.md`, "Development setup, tests and conventions: see DEVELOPMENT.md" (no duplication, §III), commit messages in the existing style (`docs(…)`, `chore: …`), and that releases are cut by maintainers (link `docs/releasing.md`)
      • T038Create `CODE_OF_CONDUCT.md` from the Contributor Covenant 2.1 text; leave the enforcement contact as the literal placeholder `[INSERT CONTACT METHOD]` and leave it for the maintainer task T049 — do not invent or insert any personal address (research R13)
      • T039Create `SECURITY.md`: supported versions table (latest `1.x` minor: supported; older: not), how to report — GitHub private vulnerability reporting (`https://github.com/SerafAC/speckit-eye/security/advisories/new`), what to include, expected first response within 7 days, coordinated disclosure; scope notes (serve mode listens on loopback only, the tool is read-only)
      • T040Create `.github/ISSUE_TEMPLATE/bug_report.yml` (fields: speckit-eye version, Node.js version, OS, command run, what happened, expected, `tasks.md`/spec excerpt if relevant), `.github/ISSUE_TEMPLATE/feature_request.yml` (problem, proposed solution, alternatives), and `.github/ISSUE_TEMPLATE/config.yml` (`blank_issues_enabled: false`; contact link "Report a security vulnerability" → the private advisory URL; contact link "Documentation" → `https://serafac.github.io/speckit-eye/`)
      • T041Create `.github/pull_request_template.md`: summary, linked spec/issue, and a checklist of the Quality Gates — unit tests (§IV), E2E tests with `USn FR-xxx` names (§V), `CHANGELOG.md` Unreleased entry for user-visible changes (§VI), `docs/` / `README.md` / `DEVELOPMENT.md` updated where affected (§VII–IX), no unjustified complexity (§I–III)
      • T042Create `.github/dependabot.yml` (contracts/workflows.md): `version: 2`; ecosystems `npm` (directory `/`) and `github-actions` (directory `/`), `schedule.interval: weekly`, one group per ecosystem for `minor` and `patch` updates (FR-031)
      • T043Add badges to the top of `README.md` under the title: CI (`https://github.com/SerafAC/speckit-eye/actions/workflows/ci.yml/badge.svg` → the workflow page), npm version (`https://img.shields.io/npm/v/speckit-eye` → `https://www.npmjs.com/package/speckit-eye`), license (`https://img.shields.io/npm/l/speckit-eye` → `LICENSE`); add a "Contributing" section at the end linking `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md` and `SECURITY.md` (FR-030; user-facing, §VIII)
      • T044Create E2E suite `tests/e2e/repo-health.spec.js` (tooling project, file checks only): `003 US5 FR-028 contribution guidelines, code of conduct and security policy exist and are linked from README`; `003 US5 FR-029 issue templates for bugs and features and a PR template with the quality gates exist` (parse the YAML templates' `name`/`body`; PR template mentions CHANGELOG and tests); `003 US5 FR-030 README shows CI, npm and license badges`; `003 US5 FR-031 Dependabot watches npm and GitHub Actions`; `003 US5 FR-014 every third-party action in .github/workflows is pinned to a commit SHA` (regex over `uses:` lines, allowing only local `./` paths and 40-hex SHAs) and `every workflow sets top-level permissions`
    • Phase 8Polish & Cross-Cutting Concerns4/7
      • T045Add entries under `## [Unreleased]` in `CHANGELOG.md` (FR-032): Added — speckit-eye is published on npm (`npx speckit-eye`); the `--home <url>` build option; a documentation website with this project's live status dashboard; contributing guide, code of conduct, security policy. Changed — screenshots moved to `docs/assets/`
      • T046Update `docs/architecture.md`: new sections "Release automation" (bump → PR → release run; `scripts/release/release.js` commands; where each FR-009–FR-017 check happens; link `docs/releasing.md`) and "Documentation site" (docmd from `docs/`, `scripts/build-site.js`, `/status/` dashboard, `pages.yml`); `--home` in the CLI section
      • T047Update `DEVELOPMENT.md`: `pnpm run docs:build` and `pnpm exec docmd dev` for previewing docs, `site/` output, the `tooling` Playwright project and the `003 USn FR-xxx` test naming for this feature, `scripts/build-site.js` and `docmd.config.js` in "Project structure"; note that the `tooling` suites need registry access (`release-package.spec.js` installs the packed tarball with `npm install`, which fetches `markdown-it`)
      • T048Run the full local validation (quickstart §1–4): `pnpm test`, `pnpm run test:e2e --project tooling`, `pnpm run test:e2e --project chromium --project chromium-nojs --project chromium-mobile`, `pnpm run docs:build && pnpm exec docmd validate`, `pnpm pack` + `pack-check`; fix any failure; run `actionlint` on `.github/workflows/*.yml` if available
      • T049Maintainer (outside the repository, cannot be automated): provide the Code of Conduct contact and replace `[INSERT CONTACT METHOD]` in `CODE_OF_CONDUCT.md`; then perform the one-time setup of `docs/releasing.md` "One-time setup" (quickstart §5)
      • T050Maintainer: after this feature is merged to `main`, confirm the Pages deploy published `https://serafac.github.io/speckit-eye/` and `/status/` (SC-005), then cut the first release following `docs/releasing.md` "The first release" — **Bump version** with `explicit` / `1.0.0`, merge the PR, review and approve the Release run — and verify (quickstart §6): `npm view speckit-eye version` → `1.0.0`, provenance shown on npmjs.com, tag `v1.0.0`, GitHub release notes = changelog section (SC-004); then switch to trusted publishing and delete `NPM_TOKEN`
      • T051Maintainer: run the negative checks of quickstart §7 — hand-pushed mismatching tag, re-dispatch on `v1.0.0` (already on npm), bump with empty Unreleased, and a tag on a scratch-branch commit with a deliberately failing unit test (fails both `ci` and the "on `main`" check) — and confirm nothing is published in any of them (SC-003); delete the scratch tag and branch
  • 002Dashboard Redesign — Sidebar Shell, Task Map, Feature Pages, Document Reader and Themes1 open85/86
    • Phase 1Setup (Shared Infrastructure)5/5Complete
      • T001Update `package.json`: add devDependencies `@fontsource-variable/geist`, `@fontsource-variable/geist-mono`, `@fontsource/instrument-serif` and `happy-dom` (latest majors); (the `tailwindcss` and `@tailwindcss/cli` `^4.1` requirement, needed for `@source inline`, is already in place since commit `441f041`); replace script `build:css` with `build:assets` = `tailwindcss -i src/styles/input.css -o dist/styles.css --minify && node scripts/copy-assets.js`; set `prepack` = `pnpm run build:assets` and `test:e2e` = `pnpm run build:assets && playwright test`; add `"dist/fonts/"` to `"files"` next to `"dist/styles.css"` (`scripts/` stays out of `files`: build-time only); run `pnpm install` to update `pnpm-lock.yaml`
      • T002Create `scripts/copy-assets.js` (research D4): export a pure `selectFontFiles(entries)` that, given `{package, files: string[]}` listings of the three `@fontsource*` packages, returns `{from, to}` pairs for only the Latin and Latin-extended `woff2` files (`*-latin-wght-normal.woff2`, `*-latin-ext-wght-normal.woff2` for the variable packages; `*-latin-400-normal.woff2`, `*-latin-400-italic.woff2`, `*-latin-ext-400-*.woff2` for Instrument Serif) plus each package's `LICENSE` as `OFL-<family>.txt` (`OFL-geist.txt`, `OFL-geist-mono.txt`, `OFL-instrument-serif.txt`), all targeted at `dist/fonts/`; and a thin `main()` (not unit-tested, one line per I/O call) that lists the package folders via `fs.readdirSync`, calls `selectFontFiles`, creates `dist/fonts/` and copies the files; run only when executed directly (`import.meta.url` check). Unit tests for `selectFontFiles` (keeps Latin/Latin-ext only, drops Cyrillic/other subsets and `.woff`, maps licence names, stable order) in `tests/unit/copy-assets.test.js`
      • T003Update `playwright.config.js` (research D16): projects `chromium` (Desktop Chrome), `firefox` (Desktop Firefox), `webkit` (Desktop Safari), `chromium-nojs` (Desktop Chrome, `javaScriptEnabled: false`, `testMatch: /nojs\.spec\.js/`) and `chromium-mobile` (`viewport: {width: 375, height: 812}`, `hasTouch: true`, `isMobile: true`, Chromium, `testMatch: /mobile\.spec\.js/`); the three desktop projects `testIgnore: [/nojs\.spec\.js/, /mobile\.spec\.js/]`; default viewport 1440 × 900 for desktop projects (SC-009); keep `workers: 1`, `retries`, `reporter`, `timeout`; update the header comment (three engines per FR-055)
      • T004Update `.github/workflows/ci.yml`: in the `e2e` job replace `playwright install --with-deps chromium` with `playwright install --with-deps chromium firefox webkit`; keep the OS matrix. The `wsl` job keeps installing Chromium only and runs its live-update suite with `--project chromium` (it tests the file watcher under WSL, not browser engines); its test path is switched to `tests/e2e/live-state.spec.js` in T073
      • T005Update `DEVELOPMENT.md`: `build:assets` instead of `build:css` (stylesheet and fonts), `happy-dom` in unit tests of `src/client/` modules, the browser-module `init`/`save` convention, the five Playwright projects and that the cloud container has Chromium only (research D16), the new `scripts/` folder in the project structure, and in the release checklist that package contents now include `dist/fonts/`
    • Phase 2Foundational (Blocking Prerequisites)18/18Complete
      • T006Rewrite the base of `src/styles/input.css` (research D3, D4, D6, D15): keep `@import "tailwindcss"`, `@plugin "@tailwindcss/typography"`, `@source "../render"`, `@source "../client"`; add `@font-face` rules for Geist (variable, `font-weight: 100 900`), Geist Mono (variable) and Instrument Serif (400 normal and italic) with `src: url(fonts/<file>.woff2) format("woff2")` **relative to the stylesheet** and `font-display: swap`; define every color token of the handoff's token table **once** on `:root` with `light-dark(<light>, <dark>)` (e.g. `--bg: light-dark(#F4F3EF, #0E1013)`), plus `--rose: light-dark(#BE123C, #FB7185)`, `--rose-bg: light-dark(#FFE4E6, #3F1522)`, `--rose-text: light-dark(#9F1239, #FDA4AF)` and a single `--next` orange `#C2410C` that is not a `light-dark()` pair (FR-047); `:root { color-scheme: light dark }`, `:root[data-theme="light"] { color-scheme: light }`, `:root[data-theme="dark"] { color-scheme: dark }`; a `.always-dark { color-scheme: dark }` utility for sidebar, rail, Up next bar and tooltip; `@theme` font families `--font-sans: "Geist Variable"`, `--font-mono: "Geist Mono Variable"`, `--font-serif: "Instrument Serif"`; `@utility w-pct-* { width: calc(--value(integer) * 1%); }` with `@source inline("w-pct-{0..100}")`; keep the global reduced-motion rule (001 FR-036); focus ring utility (visible outline on `:focus-visible`, FR-050); confirm by hand that `pnpm run build:assets` output contains `.w-pct-0` … `.w-pct-100` and the `@font-face` rules with relative `fonts/` URLs (guarded automatically by the E2E test added in T076)
      • T007Create `src/render/icons.js` (research D5): ISC licence notice of Lucide in the file header; an `ICONS` map of SVG inner markup for `eye`, `search`, `grid`, `shield`, `layers`, `sun`, `moon`, `monitor`, `chevron-right`, `chevron-up-down`, `arrow-right`, `alert-triangle`, `check`, `check-circle`, `copy`, `x`, `sort`, `menu`, `file-text`; export `icon(name, { size = 16, label = null } = {})` returning a trusted `<svg viewBox="0 0 24 24" width height fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">` (or `role="img"` + `<title>` when `label` is given); throws on unknown names. Unit tests (every name renders, no `style` attribute, unknown name throws, label → accessible) in `tests/unit/icons.test.js`
      • T008Change `src/render/site.js` (research D12): `SiteEntry.body` becomes `string | Uint8Array`; add exported types `FONT_TYPE = "font/woff2"`, `JSON_TYPE = "application/json; charset=utf-8"`, `TEXT_TYPE = "text/plain; charset=utf-8"`; `renderSite(project, { base, mode, version, generatedAt, assets })` now takes `assets = { styles, modules: {name: source}, fonts: {file: Uint8Array | string} }` and publishes `assets/styles.css`, `assets/theme.js` (body from `themeScript()` of T017, not read from disk), `assets/<module>.js` for every module (`app.js`, `prefs.js` and later modules; `live.js` only in serve mode), and `assets/fonts/<file>` with `FONT_TYPE` for `.woff2` and `TEXT_TYPE` for `.txt`; update `tests/unit/site.test.js` (binary bodies kept as `Uint8Array`, content types, `live.js` absent in static mode, font paths)
      • T009Change `src/serve/handler.js` to send `Uint8Array` bodies unchanged (no string encoding, correct `Content-Length`) while keeping every 001 header including the CSP; update `tests/unit/handler.test.js` (binary body byte-exact, CSP header unchanged)
      • T010Change `src/build/build.js` (`writeSite`) to write `Uint8Array` bodies with no encoding and string bodies as UTF-8; update `tests/unit/build.test.js`
      • T011Change `src/cli/main.js` asset loading: replace the fixed `overview.js`/`live.js` map with a list of browser modules read from `src/client/` (`app.js`, `prefs.js`, `live.js`, and the modules added by later tasks — keep the list in one exported constant `CLIENT_MODULES` in `src/cli/main.js`), `dist/styles.css`, and every file of `dist/fonts/` read as bytes (`readFile` without encoding); pass them to `renderSite` in both serve and build mode (`live.js` only in serve); update `tests/unit/main.test.js` with the injected fake file system (fonts read as bytes, module list published, clear error when `dist/fonts` is missing that tells the developer to run `pnpm run build:assets`)
      • T012Create `src/model/shares.js` (data-model "Percent shares"): export `shares(values: number[]) → integer[]` — floor each `value * 100 / sum`, give the remaining points to the largest fractional parts (ties: earlier entry first), always sums to 100, all zeros for an empty array or zero sum; export `sharesNoZero(values)` that, after `shares`, raises any non-zero value that rounded to 0 to 1 by taking 1 from the largest part (FeatureSegment `parts` rule). Unit tests (sum = 100, ties, zero sum, `[123, 32, 72, 15]`, tiny non-zero part kept at 1) in `tests/unit/shares.test.js`
      • T013Extend `src/model/task-state.js` (data-model Task `display`, `waitingOn`): keep 001's rules and precedence; add `display` = `done | next | blocked | open` mapped from 001 `completed → done`, `current → next`, `blocked → blocked`, `future → open`; add `waitingOn: string[]` = the IDs in `dependsOn` whose tasks are still open, in written order, only for `blocked` tasks (empty otherwise; missing IDs ignored); export `DISPLAY_LABEL = {done: "Done", next: "Next", blocked: "Blocked", open: "Open"}`; update `tests/unit/task-state.test.js` (T020 depends on T012 + T013 with only T012 open → `waitingOn: ["T012"]`; next beats blocked; dependency on missing ID ignored)
      • T014Extend `src/model/build-model.js` (data-model Feature): add `number` = leading digits of `dir` (`001-x` → `"001"`) or the timestamp prefix, `null` without a numeric prefix; `status` = `done | in-progress | not-started | no-tasks` from the 001 Stage per the FeatureStatus table (`complete → done`, `in-progress → in-progress`, `ready → not-started`, `empty | specified | planned → no-tasks`); `statusLabel` per the same table ("Complete", "N open", "Ready" or "N open" when it is the active feature, the stage label "Specified"/"Planned"/"Empty"); Task `anchor` = `"task-" + key` with `/` and `@` replaced by `-`; update `tests/unit/build-model.test.js`
      • T015Add feature documents to `src/model/build-model.js` (data-model DocumentGroups): `documents = { groups: [{name: "Define"|"Design"|"Contracts"|"Build"|"Other", items: Artifact[]}] (empty groups left out), tabs: [{label, count, items: Artifact[]}] }` with the artifact kind → group/tab table (spec → Define/Specification; checklist → Define/"Quality checklist" when it is the only checklist and its file is `checklists/requirements.md`, otherwise "Checklists" with count; plan, research, data-model, quickstart → Design; contract → Contracts/"Contracts" with count; tasks → Build/no tab; other → Other/"More" with count), tab order Specification, Plan, Research, Data model, Quickstart, Contracts, checklists, More; add `project.documents` for the constitution ("Project") and one group per assessment; update `tests/unit/build-model.test.js` (feature without `research.md` has no Research tab; four contracts → "Contracts" count 4)
      • T016Create `src/client/prefs.js` (research D9): export `KEYS = {theme: "sk-theme", order: "sk-order", filter: "sk-filter", map: "sk-map"}`, `DEFAULTS = {theme: "system", order: "progress", filter: "all", map: null}` and `ALLOWED` value lists (`light|dark|system`, `progress|number|least|name`, `all|open`, `stacked|grouped`); `createPrefs(storage)` returning `get(name)` (unknown or invalid stored value → default) and `set(name, value)` (ignores invalid values); every storage access wrapped in `try/catch`, so a throwing or `null` storage falls back to defaults and never throws. Unit tests (defaults, round trip, invalid values, throwing storage on get and set, null storage) in `tests/unit/client-prefs.test.js`
      • T017Theme before first paint (after T016, same file `src/client/prefs.js`) without file-reading tests (research D3, SC-005, constitution §IV): add to `src/client/prefs.js` an exported, **self-contained** function `applyStoredTheme(document, storage)` (no references outside its own body, because its source text is reused) that reads `"sk-theme"` inside `try/catch` and sets `document.documentElement.dataset.theme` to `light` or `dark`, doing nothing for `system`, a missing or invalid value, or a throwing/`null` storage; create `src/render/theme-script.js` exporting `themeScript()` that returns the classic (non-module) script text `(<applyStoredTheme source>)(document, (function () { try { return window.localStorage; } catch (e) { return null; } })());` built from `applyStoredTheme.toString()`, so the published `assets/theme.js` (T008) is generated in memory and there is no separate `src/client/theme.js` file and no entry in `CLIENT_MODULES`. Unit tests: `applyStoredTheme` against a happy-dom window with fake storages (`light`, `dark`, `system`, missing, invalid, throwing, `null`) in `tests/unit/client-prefs.test.js`; `themeScript()` evaluated with `node:vm` in a context holding a happy-dom `document`/`window` (script text only, no file access) in `tests/unit/theme-script.test.js`
      • T018Rewrite `src/render/layout.js` (contracts/routes.md "Page shell", FR-005–FR-009): export `renderPage({ title, base, mode, project, page: "overview"|"feature"|"document", current, main, version, generatedAt })`; `<head>` loads `{base}assets/theme.js` (blocking, first script), `{base}assets/styles.css`, `<script type="module" src="{base}assets/app.js">`, and `{base}assets/live.js` in serve mode only; `<body data-mode data-version data-page data-base>`; for `overview` and `feature` pages an `<aside data-region="sidebar" class="always-dark">` with brand (eye icon, "speckit-eye"), project name, `<button data-part="search" hidden>` "Search tasks, specs…" with a ⌘K hint, `<nav aria-label="Main">` (Overview, Constitution when present, "Assessment: <slug>" per assessment), `<nav aria-label="Features">` headed "Features N / M done" listing each feature in `ranks.progress` order (until T024 lands, folder order) as `<a data-key="side:<dir>" data-sig data-status href="{base}features/<dir>/index.html">` with status dot (done filled green, in progress blue ring, not started/no tasks grey), the truncated name with `title` = full name, and the open-task count or a check icon, `aria-current="page"` on `current`; `<div role="group" aria-label="Theme" data-part="theme" hidden>` with three `<button aria-pressed data-theme-choice="light|dark|system">` (sun, moon, monitor); `<footer>` "speckit-eye <version>" plus " · generated <time>" in static mode; for `document` pages a `<nav data-region="rail" class="always-dark">` with icon links to the same destinations, the search button (hidden) and the theme group; on every page a `<details data-region="mobile-menu">` whose `<summary>` (menu icon) shows only on narrow screens and contains the same navigation (works without JS, FR-009); `<main>`; `<div data-region="live-status" hidden>` in serve mode; an empty `<dialog data-region="search">`; `<div data-region="tooltip" role="tooltip" hidden>` on the overview only; no `style` attribute anywhere; remove the old header, header links and Menu (FR-008). Rewrite `tests/unit/layout.test.js` (regions per page type, script order, no `style=`, `aria-current`, static footer time, sidebar count "Features 1 / 4 done")
      • T019Add the shell's CSS to `src/styles/input.css`: sidebar (fixed width per handoff, own scroll for the Features list), icon rail, main column, mobile layout below the handoff's breakpoint (sidebar hidden, `mobile-menu` summary shown, no horizontal page scroll, FR-009), status dots, pills, chips, buttons with ≥ 36 px hit targets (FR-050)
      • T020Create `src/client/app.js` (contracts/routes.md "Browser modules"): imports `prefs.js` and the page modules; `start({ document, window, storage })` reads `document.body.dataset.page`, calls the shared `initShell(root, deps)` (un-hides and wires the theme switch: `aria-pressed` reflects `prefs.get("theme")`, a click stores the choice via prefs and sets or removes `data-theme` on `<html>` immediately, so it works even when storage fails, FR-045, US4 AC5) and then `init(root, deps)` of each module registered for that page in one `MODULES = { overview: [...], feature: [...], document: [...], all: [...] }` table; exports `save(root) → {<module>: state}` and `reinit(root, state)` for `live.js`; the file ends with a guarded bootstrap `if (typeof document !== "undefined") start(...)`; until US1/US2 replace it, the overview entry still runs the 001 `attachHover`/`attachClick` from `src/client/overview.js`. Unit tests with happy-dom (right modules per page, `reinit` passes each module its saved state, theme switch updates `aria-pressed` and `data-theme`, throwing storage still switches) in `tests/unit/client-app.test.js`
      • T021Change `src/client/live.js` (research D2, FR-051): before replacing `<main>`, call `app.save(root)` and record `window.scrollY` and `scrollTop` of every `[data-keep-scroll]` by name; after the swap keep 001 `applyToggles` and the change flash (`collectSigs`/`changedKeys`), then call `app.reinit(root, state)` and restore the scroll positions; also replace the sidebar Features list and footer (they carry `data-sig`) so sidebar counts update; emit a `sk:change` event on `document` after each swap (used by search, T069); keep the 001 SSE protocol and reconnect; update `tests/unit/client-live.test.js` (save called before swap, reinit after, scroll restored for `tree`, `tasks`, `docs`, sidebar entry flashed on change, `sk:change` dispatched once after each swap)
      • T022Add the feature page route to `src/render/site.js`: publish `features/<dir>/index.html` for every feature (contracts/routes.md) rendered by a new `renderFeaturePage(feature, project, { base })` exported from a new `src/render/feature.js`, which for now renders only `<header data-region="feature-head">` (breadcrumb "Overview / Features", status pill, `<code>dir</code>`, `<h1>` title, "done / total tasks · completed of total phases") and a plain list of the feature's documents, inside `renderPage({ page: "feature", current: dir })`; document pages (`src/render/artifact.js`) now render with `page: "document"` (icon rail) until US5 replaces them; unit tests in `tests/unit/feature.test.js` and `tests/unit/site.test.js` (one feature page per feature, sidebar marks it current)
      • T023Update the existing E2E suites to the new shell so the suite stays green: `tests/e2e/helpers.js` (selectors for `[data-region="sidebar"]`, `[data-region="rail"]`, feature page paths), `tests/e2e/security.spec.js`, `tests/e2e/us3-artifacts.spec.js`, `tests/e2e/us4-build.spec.js`, `tests/e2e/self-counts.spec.js`, `tests/e2e/scale.spec.js` (replace header/Menu navigation with sidebar or rail links; keep every assertion's intent); add to `tests/e2e/security.spec.js` a test `FR-004 serves fonts from its own origin with the unchanged CSP` (fonts load with `font/woff2`, no request leaves the origin, CSP header string unchanged)
    • Phase 3User Story 1 - See where the project stands in the new overview (Priority: P1) 🎯 MVPP111/11Complete
      • T024Create `src/model/ranks.js` (data-model FeatureRanks, research D10): export `computeRanks(features) → Map<dir, {progress, number, least, name}>` with 0-based positions: `progress` = group 1 features with open tasks (`in-progress` before `not-started`), group 2 `no-tasks`, group 3 `done`, inside each (sub-)group `dir` descending; `number` = `dir` ascending; `least` = `counts.percent` ascending, then `counts.open` descending, then `dir` ascending, features without tasks last by `dir`; `name` = `title` with `localeCompare(…, "en", {sensitivity: "base"})`, then `dir`; wire it into `src/model/build-model.js` as `feature.ranks`. Unit tests (each order on a four-feature example, ties, features without tasks) in `tests/unit/ranks.test.js`
      • T025Create `src/model/warnings.js` (data-model WarningGroup, FR-015): export `mergeLines(numbers) → {from, to}[]` (sorted, consecutive merged: 23, 254–258 → `[{23,23},{254,258}]`), `formatRange({from, to})` → `"L23"` / `"L254–258"` (en dash), and `groupWarnings(warnings, contentByFile) → WarningGroup[]` with `code`, `file`, `count`, `title` (plain sentence with the count, e.g. "6 checkboxes without a task ID in tasks.md"; one title template and `note` per 001 code `W1`–`W12` in one table, e.g. W1 note "counted, not linkable"), `lines`, `sourceLines` (`{line, text}[]` of the raw lines from the already-read content), ordered by first line, warnings without a line in their own group per code; wire into `src/model/build-model.js` as `feature.warningGroups`. Unit tests (merging, en dash, singular/plural titles, ordering, no-line warnings, every 001 code has a title) in `tests/unit/warnings.test.js`
      • T026Add `project.overview` (data-model OverviewStats, FeatureSegment, FR-011) to `src/model/build-model.js`: `percent`, `done`, `total` from `totals.tasks` (001 rounding, capped at 99 % while any task is open); `features {completed, total, inProgress}`; `phases {completed, total, remaining}`; `openTasks {count, features}`; `segments` for features with `counts.total > 0` in `dir` order with `dir`, `number`, `title`, `share` (via `shares()` over all segments' totals), `parts {done, open, next}` via `sharesNoZero` (blocked counted as open), `counts`, `showLabel = share >= 4`; `legend {done, open, blocked, next}` over all tasks (`open` excludes blocked and next). Unit tests with the spec's example (123/123, 24/32, 72/72, 14/15 → "96 %", 233 of 242, Features 2 / 4 with 2 in progress, Open 9 across 2 features, segments proportional to 123, 32, 72, 15) and the `mixed` fixture numbers in `tests/unit/build-model.test.js`
      • T027Rewrite `src/render/overview.js` page head, stats card and Up next bar (contracts/routes.md "Overview `<main>`", FR-010, FR-011, FR-013): `<header data-region="page-head">` with project name, `<h1>Project overview</h1>` and the hidden `role="group" aria-label="Show"` filter with `data-filter="all|open"` buttons; `<section data-region="stats">` with the percentage, "done of total tasks", three counters (`data-stat="features|phases|open"`, each with `data-key="stat:<name>"` and `data-sig`), the segments `<a data-key="seg:<dir>" href="#<feature anchor>" class="w-pct-N min-w-[3px]" title="<number> · <title> — d done, o open, n next">` each containing `<span data-part="done|open|next" class="w-pct-N">`, labels (feature number when `showLabel`), and a legend Done / Open / Next up; empty project → 0 % with a short explanation and an empty state instead of the bar; `<section data-region="up-next" class="always-dark">` with ID chip, task text exactly as written on one line with `title` = full text, feature › phase, "Open quickstart" (only when the active feature has a quickstart artifact) and "View task" → `features/<dir>/index.html#<anchor>`, or `data-empty="complete|no-tasks"` with "Every task is complete" / "No tasks yet"; unit tests in `tests/unit/overview.test.js` (numbers, widths as classes, no `style=`, empty and complete states, quickstart button presence)
      • T028Rewrite the tree part of `src/render/overview.js` (FR-012, FR-014–FR-018): `<section data-region="features">` header with hidden `<button data-part="order" data-order="progress">` (sort icon + label of the current order) and hidden `<div role="group" data-part="depth">` Features | Phases | Tasks; `<div data-region="tree" data-keep-scroll="tree" data-filter="all">` rendering features in `ranks.progress` order as `<li data-feature data-rank-progress data-rank-number data-rank-least data-rank-name [data-complete]>` → `<details data-key data-sig data-status [data-active] [open]>` whose `<summary>` has chevron, status dot, number chip (omitted when `number` is null), truncated title with `title`, warnings badge "N warnings" when `warningGroups` is non-empty, status pill (`statusLabel`), mini progress bar (`w-pct-<percent>`), "done/total" (no bar for `no-tasks`), then `<a data-part="open-feature">` to the feature page; an expanded feature with warnings starts with one amber `<a data-part="warning" data-code href="features/<dir>/index.html#warnings">` row per group with title, note, line chips (`formatRange`) and "Details"; phases as `<details data-key data-status [data-complete]>` with "Phase N", title, priority badge (P1 filled dark, P2 grey, P3 outlined) when the phase belongs to one story, "done/total" plus check when complete or "—" and no chevron when empty; 001 story sub-level kept; task rows `<li data-key id="<anchor>" data-state="done|next|blocked|open" data-sig>` with mark, `<a href="features/<dir>/index.html#<anchor>">ID</a>` ("No ID" as plain text for tasks without an ID), raw text on one line with `title`, and a "NEXT" badge + tint on the next task; only the active chain open on load (001 FR-016); keep the 001 grid in the right column until T039 replaces it. Unit tests in `tests/unit/overview.test.js` (rank attributes, warning row "6 checkboxes without a task ID in tasks.md" with "L23" and "L254–258", badge "6 warnings", empty phase "—", NEXT badge, blocked `data-state`)
      • T029Switch the sidebar Features list in `src/render/layout.js` to `ranks.progress` order and mark the entry of the current feature page; update `tests/unit/layout.test.js`
      • T030Add overview CSS to `src/styles/input.css`: page head, stats card, segmented bar and legend, Up next bar (always dark, one-line truncation), two-column grid (tree left, map column right, card tops aligned; stacked with the tree first on narrow screens, FR-010), tree card with its own scroll above about 800 px (FR-018, handoff), feature/phase/task rows, amber warning rows and L-chips, pills and priority badges, mini bars, `[data-filter="open"] [data-complete], [data-filter="open"] li[data-state="done"] { display: none }` (FR-019), `[data-selected]` outline, and `[data-state]` marks including the rose blocked mark (FR-022)
      • T031Create `src/client/tree.js` (FR-012, FR-019, FR-020, research D10): `init(root, { document, storage, state })` un-hides the order button, depth group and view filter; **order**: applies `prefs.get("order")` by sorting the feature `<li>` elements on `data-rank-<order>` (moves nodes, never re-renders), each click cycles progress → number → least → name → progress, stores it and sets the button label ("In progress first", "Number", "Least complete", "Name A–Z"); **depth**: "Features" closes every feature and phase `<details>`, "Phases" opens features and closes phases, "Tasks" opens features and every phase with tasks (and story levels); the chosen depth shows `aria-pressed`, and a manual toggle of any row clears it; **filter**: sets `data-filter` on the tree from `prefs.get("filter")`, buttons store it; exports `reveal(root, anchor)` (opens the task's feature, phase and story `<details>`, scrolls the tree card — not the window — so the row is visible, sets `data-selected` on the row and clears it from any other row; a click elsewhere clears it) for `taskmap.js`; `save(root)` returns `{depth}`. Unit tests with happy-dom (sort by each rank, cycle and label, depth open/close sets, manual toggle clears depth, filter attribute, prefs round trip, reveal opens ancestors and selects, save/init restores depth) in `tests/unit/client-tree.test.js`
      • T032Register `tree.js` for the `overview` page in `src/client/app.js`, add `tree.js` to `CLIENT_MODULES` in `src/cli/main.js`, and update `tests/unit/client-app.test.js` and `tests/unit/main.test.js`
      • T033Rewrite `tests/e2e/us1-overview.spec.js` against `mixed` and `nonstandard`: `US1 FR-005 FR-006 sidebar lists features in progress-first order with dots and counts`; `US1 FR-011 stats card shows 62 % and 40 of 65 tasks` (plus features, phases, open counters); `US1 FR-011 segmented bar has one segment per feature with tasks, widths 46/31/23 %` (compare element box widths within 1 %); `US1 FR-013 Up next bar names the next task with full text on hover and View task opens the feature page`; `US1 FR-012 order cycles through four orders and the label names the current one`; `US1 FR-012 depth Tasks expands every phase with tasks and Features collapses all`; `US1 FR-015 warning row shows count, line chips and Details link` (nonstandard); `US1 FR-019 Open tasks only hides complete features and done tasks`; `US1 FR-020 order and filter are remembered after reload`; `US1 FR-006 sidebar entry opens the feature page and marks it current`; `US1 SC-001 percentage, in-progress feature and next task visible without scrolling at 1440 × 900`; `US1 SC-002 View task shows the next task's full text and files in one click, and any task is two clicks from the map (square, then its ID)`; `US1 FR-018 tree scrolls inside its card above about 800 px`; `US1 SC-009 sidebar left of content, tree left of map` (bounding boxes)
      • T034Add `CHANGELOG.md` lines under `## [Unreleased]` for the new sidebar shell, stats card, Up next bar and feature tree (order, depth, open-only filter, warning rows), and update `tests/fixtures/projects/README.md` with the expected stats, segment shares and ranks of each fixture
    • Phase 4User Story 2 - Explore every task on the task map (Priority: P2)P28/8Complete
      • T035Create `src/render/taskmap.js` (FR-021–FR-023, FR-027, FR-028, contracts/routes.md): move `GRID_ROWS_THRESHOLD`, `GRID_BARS_THRESHOLD` and `gridLayout` from `src/render/overview.js` and rename the layouts to `stacked` (≤ 1,000 tasks), `grouped` (1,001–5,000) and `bars` (> 5,000); export `renderTaskMap(project, { base })` returning `<section data-region="taskmap" data-layout>` with a header (title "Task map" and hidden `<button data-part="map-mode" aria-pressed>` reading "By feature" / "Stack all", absent for `bars`) above the card; the grid as `<a data-key data-sig data-state data-parents="<dir> <phaseKey> [groupKey]" href="#<anchor>" title="<ID or No ID> · <state label> — <text> — <feature>" aria-label=…>` per task including checkboxes without an ID, in folder and task order (never the tree order); grouped mode as `<div data-part="group">` with feature name, done/total and its squares; bars mode as one progress bar per feature (`w-pct`); `<ul data-part="legend">` "Done N · Open N · Blocked N · Next N · 1 dot = 1 task" from `project.overview.legend` (what one mark stands for in bars mode); an empty state for a project without tasks. Unit tests in `tests/unit/taskmap.test.js` (one square per task incl. no-ID, order independent of ranks, thresholds 1,000/1,001/5,000/5,001, legend sums to total, blocked `data-state`, no `style=`)
      • T036Replace the 001 grid in `src/render/overview.js` with `renderTaskMap` and remove the moved grid code and its tests from `tests/unit/overview.test.js`
      • T037Add task map CSS to `src/styles/input.css`: square sizes for stacked and grouped modes per handoff, colors done (filled green), open (blue outline), blocked (`--rose` 1.5 px outline + `--rose-bg` tint), next (filled `--next`); hover/`:focus-visible` growth `transform: scale(1.6)` with a two-ring outline within 120 ms ease-out affecting no other square (FR-024); tooltip (always dark, status pill, `line-clamp: 2` with `-webkit-line-clamp` fallback, fade); tree tint classes `[data-hl="ancestor"]` (light) and `[data-hl="deepest"]` (stronger + outline) (FR-025); growth and fade off under reduced motion
      • T038Create `src/client/taskmap.js` (research D8, FR-024–FR-026, FR-028): `init(root, { document, window, storage, setTimeout, clearTimeout, state })`: **mode**: un-hide the toggle, apply `prefs.get("map")` (falling back to the server's `data-layout`), toggle between stacked and grouped by setting `data-layout` and the button text/`aria-pressed`, store it; **tooltip**: one shared `[data-region="tooltip"]`; delegated `pointerenter` on squares starts a 500 ms timer (`TOOLTIP_DELAY_MS = 500`) unless `pointerType === "touch"`; keyboard `focus` shows at once; `pointerleave`/`blur` clears the timer and hides at once; content = ID (or "Checkbox without a task ID"), status pill (`DISPLAY_LABEL`), text, feature name — all set via `textContent`; position above the square, aligned left, centre or right by available room and clamped to the card and the viewport via CSSOM `style.left/top` (exported pure `placeTooltip(squareRect, tipRect, boundsRect) → {left, top, align}`); **highlight**: while a square is hovered or focused, set `data-hl="ancestor"` on its visible feature/phase/story rows from `data-parents` and `data-hl="deepest"` on the deepest visible one (task row if visible), cleared on leave; **click / Enter**: `preventDefault` and call `tree.reveal(root, anchor)`; touch tap does the same with no tooltip; `save(root)` returns nothing extra (mode is a preference). Unit tests with happy-dom and fake timers (no tooltip before 500 ms, shown at 500 ms, hidden on leave, focus shows at once, touch never shows, `placeTooltip` left/right/centre and clamping, no-ID text, highlight deepest visible, click calls reveal, mode persisted) in `tests/unit/client-taskmap.test.js`
      • T039Register `taskmap.js` for the `overview` page in `src/client/app.js`, remove the 001 `attachHover`/`attachClick` usage, delete `src/client/overview.js` and `tests/unit/client-overview.test.js` (§II), add `taskmap.js` to `CLIENT_MODULES` in `src/cli/main.js`; update `tests/unit/client-app.test.js` and `tests/unit/main.test.js`
      • T040Create `tests/e2e/us2-taskmap.spec.js` (moving the 001 grid tests from `us1-overview.spec.js`): `US2 FR-021 FR-023 map shows 65 squares and legend Done 40 · Open 23 · Blocked 1 · Next 1`; `US2 FR-021 By feature regroups into labelled blocks and Stack all restores`; `US2 FR-024 square grows on hover and tooltip appears after 500 ms, not before` (measure with `page.clock` or timestamps: 400–600 ms); `US2 FR-024 tooltip hides at once on leave`; `US2 FR-024 FR-050 keyboard focus on a square shows the tooltip at once and Enter reveals the task in the tree`; `US2 FR-024 tooltips of first- and last-column squares are fully inside card and viewport`; `US2 FR-025 hovering a square in a collapsed feature tints its feature row`; `US2 FR-026 clicking a square in collapsed 003-gamma opens feature and phase, scrolls the tree card and outlines the row; its ID opens the feature page`; `US2 FR-022 T018 is blocked in map and tree and its tooltip pill reads Blocked`; `US2 FR-022 after checking T011 in tasks.md T018 becomes open` (live update on a fixture copy); `US2 FR-028 map mode is remembered after reload`; `US2 FR-024 checkbox without an ID shows "Checkbox without a task ID"` (nonstandard)
      • T041Update `tests/e2e/scale.spec.js` for the 50 × 2,000 fixture: `US2 FR-027 map opens By feature above 1,000 tasks`, `US2 SC-006 hover growth within 150 ms and tooltip at 500 ± 100 ms with 2,000 squares`, `US1 FR-011 with 50 features, segments too narrow for a label show none but stay visible`
      • T042Add `CHANGELOG.md` lines for the task map (modes, blocked color, tooltip, reveal in tree)
    • Phase 5User Story 3 - Work through a feature on its feature page (Priority: P2)P211/11Complete
      • T043Create `src/model/task-files.js` (data-model Task `files`, `kind`, `refs`, TaskKind; FR-035, FR-037): export `taskFiles(description)` = backticked spans matching `/^[^\s`]*\/[^\s`]*[A-Za-z0-9_-]\.[A-Za-z0-9]+$/` or ending in `Makefile`/`Dockerfile`, in order, duplicates removed; `taskKind(file) → {label, test} | null` with test when the file name matches `/(^|[._-])(test|spec)s?([._-]|$)/i` or a folder is `test`, `tests`, `__tests__` or `e2e`, and label from the extension table (`.go` Go, `.vue` Vue, `.js`/`.mjs`/`.cjs` JavaScript, `.jsx` JSX, `.ts`/`.mts`/`.cts` TypeScript, `.tsx` TSX, `.py` Python, `.rb` Ruby, `.rs` Rust, `.java` Java, `.kt` Kotlin, `.swift` Swift, `.cs` C#, `.php` PHP, `.css` CSS, `.scss` SCSS, `.html` HTML, `.svelte` Svelte, `.md` Markdown, `.json` JSON, `.yml`/`.yaml` YAML, `.toml` TOML, `.sh` Shell, `.sql` SQL; other → extension upper-cased without the dot; `Makefile`/`Dockerfile` → the name); `kindChip(kind)` = `label` or `label + " test"`; `taskRefs(description)` = `FR-\d+[a-z]?` and `SC-\d+` in order, deduplicated; wire into `src/model/build-model.js` (`task.files`, `task.kind`, `task.refs`) and add `feature.kindChips` (distinct chip texts without " test", first appearance order). Unit tests (`internal/input/simulator_test.go` → "Go test"; two files; no file; `.proto` → PROTO; `tests/e2e/x.spec.js` → "JavaScript test"; URL-like spans without `/` ignored; refs) in `tests/unit/task-files.test.js`
      • T044Add `feature.phaseShares` to `src/model/build-model.js`: `shares()` over each phase's task total (phases without tasks get 0; rendered at the minimum width, FR-032); unit tests in `tests/unit/build-model.test.js`
      • T045Extend `src/render/markdown.js`: export `renderInline(md, text)` using the shared `markdown-it` instance's `renderInline` (raw HTML off, 001 link rules); make `tasks.md` documents wrap every task line in `<span id="L<line>">` (FR-044; line numbers from the source, fences and comments excluded as in 001 `visibleLines`) with `:target` highlight CSS in `src/styles/input.css`; update `tests/unit/markdown.test.js` (inline HTML stays text, task lines addressable)
      • T046Create `src/render/task-text.js` (contracts/routes.md "Inline task text", FR-035, FR-037): export `renderTaskText(md, task)` → inline HTML of `description` via `renderInline` with `` `code` `` as mono chips and `FR-…`/`SC-…` references wrapped as `<span class="chip" data-ref>` chips (only in text nodes, never inside code or link text); `renderKindChip(task)` (fixed-width chip, empty placeholder of the same width when `kind` is null); `renderFileChip(task)` (file name of the single file with `title` = full path, "N files" when several, empty placeholder when none); `renderMarkers(task)` (USn, "Parallel", FR/SC refs, "depends on …"). Unit tests (text never altered, `<script>` in text stays text, chips aligned by fixed classes, "2 files") in `tests/unit/task-text.test.js`
      • T047Complete `src/render/feature.js` header and tabs (FR-030, FR-031): header with breadcrumb (Overview / Features), status pill, `<code>dir</code>`, `<h1>`, "done / total tasks · completed of total phases" and an SVG progress ring (`stroke-dasharray` from `counts.percent`, check icon when complete); `<nav data-region="tabs" aria-label="Documents">` with "Tasks <n>" (`aria-current="page"`) then one tab per `feature.documents.tabs` entry in order, a tab with one document links to its page, a tab with several renders `<details data-part="tab-menu"><summary>Contracts <n></summary>` listing them (FR-008); unit tests in `tests/unit/feature.test.js` (full tab set, no Research tab without `research.md`, "Contracts 4", "Quality checklist" vs "Checklists 2", "More")
      • T048Add the Tasks tab body to `src/render/feature.js` (FR-032–FR-035, contracts/routes.md "Feature page `<main>`"): `<nav data-part="rail">` with one `<a href="#phase-<n>" class="w-pct-N" data-status>` per phase (`phaseShares`, label-sized minimum width, short name "P4" and task count, narrow non-openable block for empty phases) and `<p data-part="caption">` "Width = task count · Selected: <phase or None>"; `<section id="warnings" data-part="warnings">` amber banner when `warningGroups` is non-empty with titles, line chips and `<details><summary>Show lines</summary><ol>` of `sourceLines`; hidden `<div data-part="filters">` with chips All (count), Open (count, blocked included), Tests, one chip per `feature.kindChips`, `<input type="search">` and "Expand all" / "Collapse all"; `<div data-part="list" data-keep-scroll="tasks">` with `<details name="phases" data-key id="phase-<n>" data-status [open]>` per phase (active phase open, none when no open tasks) whose summary has "Phase N", title, priority badge and done/total; task rows `<details data-key id="<anchor>" data-state data-kind data-test? data-files="<names>" data-text="<lower-cased id + text>">` with `<summary>` mark, ID ("No ID" without address for ID-less tasks), `renderTaskText`, kind chip, file chip; body: full wrapped text and `renderMarkers`; hidden `<aside data-region="detail">`; a feature without `tasks.md` shows an empty Tasks tab explaining its stage. Unit tests in `tests/unit/feature.test.js` (rail classes, active phase open, banner lines, filter chip set, row attributes, empty stage)
      • T049Add feature page CSS to `src/styles/input.css`: header and ring, tabs (scroll sideways on narrow screens), rail (scroll sideways on narrow screens) and caption, amber banner, filter chips and search input, task list columns with fixed-width kind and file chips so they line up, one-line truncation in summaries and wrapped text when open, detail panel beside the list (stacked below it on narrow screens), selected-row outline, `:target` highlight
      • T050Create `src/client/feature.js` (FR-032–FR-036): `init(root, { document, window, navigator, setTimeout, state })`: **rail ↔ list**: choosing a rail block or a phase summary opens that phase (native `<details name="phases">` closes the others; for browsers without exclusive groups, close the others in JS), choosing the open one closes it, `aria-current` on the rail block and the caption follow the open phase; **filters**: un-hide the filter bar; chips (All/Open/Tests/kind) and the text filter combine (text matches `data-text` and `data-files`); while any filter is active every phase with matches is open and phases without matches are hidden; each chip shows its match count; no match → "No tasks match" with a "Clear filters" button; **expand/collapse all**; **selection**: opening a task row selects it (second click collapses), fills and un-hides the detail panel (ID, status via `DISPLAY_LABEL`, full text, phase, markers, file paths, "Waiting on T012, T013" from `data-waiting-on` for blocked tasks, "Copy ID", "View source line" → `tasks.html#L<line>`, close button) and sets the address fragment with `history.replaceState`; **address**: on init a `#task-…` fragment opens its phase, expands and selects the task and scrolls it into view (FR-036, US3 AC10); **copy**: `navigator.clipboard.writeText` with a text-selection fallback and a short "Copied" confirmation; `save(root)` returns `{chips, text, selected}`; the renderer (T048) adds `data-waiting-on`, `data-line` and `data-phase` to task rows for this module. Unit tests with happy-dom (accordion incl. choose-again-to-close, filter combinations and counts, empty-result message and clear, selection fills panel incl. "Waiting on T012", fragment on init, copy success and fallback, save/init restores) in `tests/unit/client-feature.test.js`
      • T051Register `feature.js` for the `feature` page in `src/client/app.js` and add it to `CLIENT_MODULES` in `src/cli/main.js`; make the Up next "View task", tree task IDs and search results link to `features/<dir>/index.html#<anchor>` (already rendered; verify with a unit test in `tests/unit/overview.test.js`); update `tests/unit/client-app.test.js`
      • T052Add backticked file paths to a few `002-beta` task texts in `tests/fixtures/projects/mixed` (for example `src/list.go`, `src/list_test.go`, `web/List.vue`; checkbox states and counts unchanged) so the kind and Tests chips have data, and list them in `tests/fixtures/projects/README.md`; then create `tests/e2e/us3-feature.spec.js` on `mixed`, `complete` and `nonstandard`: `US3 FR-030 header shows pill, folder, title, counts and ring` (and "Complete" + check for a complete feature); `US3 FR-031 tabs list only existing documents in order`; `US3 FR-008 Contracts tab lists its documents without a navigation`; `US3 FR-032 rail block widths proportional to task counts`; `US3 FR-032 choosing a phase opens it and closes the other; choosing it again closes it`; `US3 FR-034 Open filter lists 10 open tasks and hides phases without matches`; `US3 FR-034 text filter narrows and a nonsense word shows No tasks match with Clear filters`; `US3 FR-034 Tests chip and each kind chip filter the list and show their counts`; `US3 duplicate task IDs can each be selected on their own` (nonstandard); `US3 FR-035 kind and file chips line up in columns` (bounding boxes); `US3 FR-036 clicking T018 expands it and the detail panel says Waiting on T011`; `US3 FR-036 Copy ID puts T018 on the clipboard` (grant clipboard permissions in Chromium; Firefox/WebKit check the confirmation only); `US3 FR-044 View source line opens tasks.html#L31 highlighted`; `US3 FR-036 opening an address with #task-… shows the task open, selected and in view`; `US3 FR-033 amber banner and Show lines reveal the source lines` (nonstandard); `US3 SC-009 detail panel right of the task list`
      • T053Add `CHANGELOG.md` lines for feature pages (tabs, phase rail, filters, kind/file chips, detail panel, Copy ID, View source line, addressable tasks)
    • Phase 6User Story 4 - Choose a light, dark or system theme (Priority: P2)P24/4Complete
      • T054Verify and complete the always-dark regions and token coverage in `src/styles/input.css` (FR-047): `.always-dark` on sidebar, icon rail, Up next bar and map tooltip in `src/render/layout.js`, `src/render/overview.js` and `src/render/taskmap.js`; `--next` identical in both themes; done/open/blocked/next/warning keep their color family; add a unit test in `tests/unit/layout.test.js` that the rendered sidebar, icon rail, Up next bar and tooltip carry `always-dark` (rendered HTML only, no file access, §IV); the identical next orange in both themes is checked end to end in T056 (`US4 FR-047`, computed colors)
      • T055Make the theme switch follow OS changes in System mode in `src/client/app.js` (FR-045): nothing to do in JS when `data-theme` is absent (CSS `color-scheme: light dark` follows the OS), but keep the switch's `aria-pressed` correct after live updates (`reinit`) and across pages; ensure `live.js` never touches `<html data-theme>`; add unit tests in `tests/unit/client-app.test.js` (reinit keeps the pressed button, System removes `data-theme`)
      • T056Create `tests/e2e/us4-theme.spec.js`: `US4 FR-045 first visit shows System and follows the OS scheme` (`page.emulateMedia({colorScheme})`); `US4 FR-045 System follows an OS switch without reload`; `US4 FR-046 SC-005 Dark applies from first paint on another page and after reload` (assert `data-theme="dark"` and the computed `--bg` before `DOMContentLoaded` handlers run, e.g. via `page.addInitScript` observer recording the first paint's background); `US4 FR-046 theme survives a live update`; `US4 FR-047 sidebar, Up next bar and tooltip are dark and next orange is identical in both themes`; `US4 FR-046 storage refused: the switch changes the current page and the next page is System` (`addInitScript` making `localStorage` throw); `US4 SC-010 body text contrast ≥ 4.5:1 in both themes on overview, feature and document pages` (compute from computed colors, including the rose pill)
      • T057Add `CHANGELOG.md` lines for the theme switch
    • Phase 7User Story 5 - Read specs and other documents in the document reader (Priority: P3)P39/9Complete
      • T058Create `src/parse/spec-structure.js` (contracts/spec-md-structure.md, research D13): export `parseSpecStructure(text) → {metadata, request, blocks}`; blocks in file order with 1-based inclusive `from`/`to`, **disjoint and covering every line**; never throws, never rewrites text; lines inside fences and HTML comments (001 `visibleLines`) never matched; recognize `title` (first `# ` heading), metadata (`**Feature Branch**:`, `**Created**:`, `**Status**:` before the first `## `, code spans unwrapped in branch, missing fields omitted, `null` when none), request (`**Input**:` with a leading `User description:` and one pair of surrounding `"…"`/`“…”` removed for display only), `section` for every `## ` heading (`number` two digits, `heading`, `anchor` with 001 `slugify`), `clarifications` (`## Clarifications` → sessions from `### Session <date>`, items `- Q: … → A: …` or `->` with indented continuation lines, badge `yes`/`no`/`neutral` from the answer's first word after trimming punctuation), `story` (`### User Story <n> - <title> (Priority: P<k>)` up to the next `### `, `## ` or `---`; `description`, `**Why this priority**:` → `why`, `**Independent Test**:` → `test`, `**Acceptance Scenarios**:` numbered items → `scenarios` with `given`/`when`/`then` only when exactly one of each appears in that order, trailing `,`/`;` trimmed, else `raw`), `requirements` (`### Functional Requirements` → areas from bold-only paragraphs, items `**FR-<id>**:` with continuation lines, unnamed area for items before the first area, other lines as `plain`), `entities` (`### Key Entities` → `- **<name>**: <description>` items, others `plain`), everything else `plain` with its Markdown source. Unit tests for every rule and each example row of the contract, plus the **coverage invariant** asserted over in-memory sample texts only (constitution §IV: no file access in unit tests): a new `tests/unit/spec-samples.js` exporting template strings — a full template-conformant spec, one with every deviation of the contract (non-template sections, clarifications not in `Q: … → A:` form, scenarios without a single Given/When/Then, fences and HTML comments containing look-alike lines, empty file, file without `## ` headings) and an excerpt reproducing the structure of this repository's 001 spec — in `tests/unit/spec-structure.test.js`. The same invariant over the real files of `tests/fixtures/projects/**` and this repository's `specs/**` is checked end to end in T065 (SC-012)
      • T059Add `feature.spec` to `src/model/build-model.js` (`parseSpecStructure` of the already-read `spec.md`, `null` without it) and resolve each story's **phase links** (phases whose `mergedStory` or groups carry the story's label, with their counts); unit tests in `tests/unit/build-model.test.js`
      • T060Create `src/render/spec-view.js` (FR-042, FR-043, contracts/spec-md-structure.md "Rendering notes"): export `renderSpecBlocks(spec, { md, feature })` → HTML: metadata card (branch, created, status); request as a pull quote; numbered `section` headings with anchors; clarifications "N answered · M sessions" in the heading, sessions as `<details data-key>` (first open) with question count, items as question, fixed-width badge ("Yes", "No", "A") and answer text in one aligned column, the first three visible and the rest in `<details data-part="more"><summary>Show N more answers</summary>`; stories as `<details data-key>` cards (first open, others collapsed rows with ID, priority badge, title, scenario count) with description, "Why this priority", "Independent test" boxes, a numbered Given / When / Then table (`raw` scenarios as one full-width row) and the linked phase with done/total; requirements with hidden area chips (`data-part="areas"`, "All" default), area headings, each requirement with its ID chip and `MUST NOT`, `MUST`, `SHOULD NOT`, `SHOULD`, `MAY` as whole upper-case words wrapped in `<mark data-kw>` (longest first, text nodes only); entities as a two-column card grid; `plain` blocks via the 001 Markdown renderer; all text through the shared `markdown-it` instance. Unit tests (each structure, badges, "Show 10 more answers", keyword highlight not inside code, `<script>` stays text, **every non-blank source line's text appears in the output** for every sample of `tests/unit/spec-samples.js`) in `tests/unit/spec-view.test.js`
      • T061Create `src/render/reader.js` replacing `src/render/artifact.js` (FR-038–FR-041, FR-044, contracts/routes.md "Document page `<main>`"): export `renderDocument(artifact, { base, project, feature, assessment, md })` rendering, inside `renderPage({ page: "document" })`, `<nav data-region="doc-list" data-keep-scroll="docs">` (back link to the feature page, feature title and status, groups Define/Design/Contracts/Build/Other from `feature.documents.groups` with contracts by title, empty groups left out, current marked `aria-current`; project documents get the project document list), `<article data-region="doc" data-key="<source path>">` with header (eyebrow "<KIND> · <file>", `<h1>`, hidden `<button data-part="expand-all">`, `<details data-part="raw"><summary>Raw markdown</summary><pre>` exact source`</pre></details>`), then `renderSpecBlocks` for `spec.md` or the 001 Markdown rendering with top-level sections numbered 01, 02, … (tasks.md keeps the `L<line>` anchors, T045), and `<nav data-region="toc" aria-label="On this page">` listing sections (and, for a specification, its stories under the user-scenarios section as `US1 · <title>`) with a hidden `<div data-part="progress">`; the 001 deleted-document notice stays; update `src/render/site.js` to use it; delete `src/render/artifact.js` and move/replace `tests/unit/artifact.test.js` with `tests/unit/reader.test.js` (group placement, current mark, numbering, raw source exact, TOC entries, project list)
      • T062Add reader CSS to `src/styles/input.css`: three-column reader (doc list, reading column with comfortable measure in Instrument Serif headings / Geist body per handoff, contents panel), metadata card, pull quote, clarification rows with the badge column aligned, story cards and Given/When/Then table, requirement chips and `mark[data-kw]`, entity grid; on narrow screens hide the contents panel and put the document list behind a `<details>` control
      • T063Create `src/client/reader.js` (FR-040–FR-042): `init(root, { document, window, IntersectionObserver, state })`: **contents**: observe section headings, mark the section in view (`aria-current="location"`) and set the progress indicator (`w-pct-N` class from scroll position; un-hide it), links jump to sections; **expand all**: un-hide the button, open every `<details>` inside the article (and toggle to "Collapse all"); **raw view**: with JS the `Raw markdown` control swaps the formatted view for the source view and back; **show more** answers; **requirement area chips**: un-hide, "All" default, picking an area hides the other areas' requirements; `save(root)` returns `{raw, expanded, area, more}`. Unit tests with happy-dom and a fake `IntersectionObserver` (current section, progress class, expand all, raw toggle, area filter, save/init restore) in `tests/unit/client-reader.test.js`
      • T064Register `reader.js` for the `document` page in `src/client/app.js`, add it to `CLIENT_MODULES` in `src/cli/main.js`; update `tests/unit/client-app.test.js`
      • T065Create `tests/e2e/us5-reader.spec.js` (replacing `tests/e2e/us3-artifacts.spec.js`, whose still-valid 001 checks — every artifact reachable, raw HTML shown as text, deleted-document notice — move here), serving this repository and the `artifacts` fixture: `US5 FR-038 FR-039 document list groups Define, Design, Contracts, Build, Other with the current document marked`; `US5 FR-042 metadata card and pull quote`; `US5 FR-042 clarifications show N answered · M sessions, first session open, Show N more answers and aligned badges` (badge column x-positions equal); `US5 FR-042 user story card has why, independent test, Given/When/Then table and phase progress`; `US5 FR-042 area chip narrows requirements and MUST is highlighted`; `US5 FR-041 contents panel marks the section in view and progress advances on scroll`; `US5 FR-040 Raw markdown shows the exact source and toggles back`; `US5 FR-040 Expand all opens every collapsed part`; `US5 FR-043 SC-012 every non-blank source line appears in the formatted view` (for every `spec.md` of this repository's `specs/*/` and of `tests/fixtures/projects/**`, read by the test runner, which is allowed in E2E tests); `US5 FR-043 a non-template section renders as ordinary text`; `US5 FR-007 document pages show the icon rail instead of the sidebar`; delete `tests/e2e/us3-artifacts.spec.js`
      • T066Add `CHANGELOG.md` lines for the document reader and the structured `spec.md` view
    • Phase 8User Story 6 - Find a task, feature or document with search (Priority: P3)P36/6Complete
      • T067Create `src/render/search-index.js` (contracts/search-index.md, research D14): export `buildSearchIndex(project) → {version: 1, entries}` with entries in page order (features in folder order, each followed by its tasks in file order and its documents in 001 artifact order with every `##`/`###` heading, then project documents): `task` (`label` ID or "No ID", `detail` text as written, `context` "<number> · <title>", `state` display state, `url` `features/<dir>/index.html#<anchor>` or `#phase-<n>` for ID-less tasks), `feature` (`label` "<number> · <title>", `detail` "<statusLabel> · d/t", `url`), `document` (title, file name, context "Project" or "Assessment: <slug>" for project documents), `heading` (heading text, document title, `url` with the 001 slug anchor); `terms` = lower-cased `label + " " + detail` for tasks and documents, `label` only for features and headings (decisions.md), with whitespace collapsed; no document body text. Unit tests (order, every field, headings from `##`/`###` only, body words absent, sub-path-free URLs) in `tests/unit/search-index.test.js`
      • T068Publish `assets/search-index.json` from `src/render/site.js` with `JSON_TYPE` in both modes; update `tests/unit/site.test.js`
      • T069Create `src/client/search.js` (FR-049–FR-049c, contracts/search-index.md "Matching"): export pure `search(entries, query) → {groups: [{name: "Tasks"|"Features"|"Documents", items, more}], empty}` (lower-case, split on whitespace, empty query → nothing, every word must occur in `terms`, ranking: exact full task ID first, then `label` starting with the first word, then the rest, ties in page order; documents and headings in one group; first 8 per group plus "N more"); `init(root, { document, window, fetch, navigator })` un-hides every `[data-part="search"]` entry, opens `<dialog data-region="search">` with `showModal()` on ⌘K (macOS, from `navigator.platform`/`userAgentData`) or Ctrl+K and on entry click, focuses the input, fetches `{base}assets/search-index.json` on first use (relative to `document.body.dataset.base`) and again on the next use after a `sk:change` event, renders results (tasks with state mark, ID, text and feature; features with number, name, status; documents/headings with title and feature) via `textContent`, arrow keys move the active option (`aria-activedescendant`), Enter or click navigates to `base + url`, "N more" expands the group, Escape closes and focus returns to the opener, "No results for "<query>"" when nothing matches. Unit tests (matching and ranking rules, 8 + more, empty query, no match; happy-dom: shortcut opens, Escape restores focus, arrow/Enter navigation, fetch once then again after `sk:change`, base prefix) in `tests/unit/client-search.test.js`
      • T070Register `search.js` for all pages in `src/client/app.js` and add it to `CLIENT_MODULES` in `src/cli/main.js`; add dialog and result CSS to `src/styles/input.css`; update `tests/unit/client-app.test.js`
      • T071Create `tests/e2e/us6-search.spec.js`: `US6 FR-049 Ctrl/⌘K and the sidebar entry open search with the cursor in the box; Escape closes and returns focus`; `US6 FR-049a T018 is the first result with state mark, text and feature`; `US6 FR-049a every typed word must match across groups`; `US6 FR-049b Enter on a task opens its feature page with the task selected and in view`; `US6 FR-049b a heading result opens the reader at that heading`; `US6 FR-049a no match shows the message`; `US6 FR-049a a word only in a document body finds nothing`; `US6 FR-049c static build under /eye/ searches the same without other hosts` (build with `--base /eye/`, serve under the sub-path, record requests); `US6 FR-049c a task added to tasks.md is found after the live update without reload`
      • T072Add to `tests/e2e/scale.spec.js`: `US6 SC-014 results update within 200 ms per keystroke with 2,000 tasks and a full ID is first`; add `CHANGELOG.md` lines for search
    • Phase 9Polish & Cross-Cutting Concerns13/14
      • T073Create `tests/e2e/live-state.spec.js` (replacing `tests/e2e/us2-live.spec.js`, whose 001 checks move here), following quickstart Scenario 6: `US1 FR-020 FR-051 order, filter, depth, expanded rows and tree scroll survive a live update`; `US2 FR-028 FR-051 map mode survives a live update`; `US3 FR-051 feature filters, text filter, open phase, selected task and list scroll survive a live update`; `US5 FR-051 raw view, expansions and area chip survive a live update`; `FR-051 changed counts are briefly highlighted (stats, segments, sidebar)`; `FR-001 SC-008 changes appear within 2 s`; `US3 FR-051 deleting the selected task clears the selection`; delete `tests/e2e/us2-live.spec.js` and, in the same change, point the `wsl` job of `.github/workflows/ci.yml` at `pnpm run test:e2e tests/e2e/live-state.spec.js --project chromium`
      • T074Create `tests/e2e/nojs.spec.js` (runs in `chromium-nojs`): `FR-053 SC-011 overview tree, phases and task rows open and close without scripts`; `FR-053 map squares jump to their tree rows`; `FR-053 feature page tabs, phases, task rows and Show lines work`; `FR-053 reader clarification sessions, stories, Show more answers and Raw markdown open and close`; `FR-053 FR-009 mobile menu opens without scripts`; `FR-048 page follows the OS theme`; `FR-053 search entry, theme switch, order, depth and filters are not shown`
      • T075Create `tests/e2e/mobile.spec.js` (runs in `chromium-mobile`): `FR-009 SC-009 every page type has no horizontal page scroll at 375 px`; `FR-009 sidebar is behind the menu control`; `FR-010 tree comes before the map`; `FR-024 FR-026 tapping a square reveals its task without a tooltip`; `FR-031 FR-032 tabs and phase rail scroll sideways inside their own area`; `FR-039 reader hides the contents panel and puts the document list behind a control`; `FR-050 buttons and links have hit targets of at least 36 px` (squares excepted)
      • T076Update `tests/e2e/us4-build.spec.js`: `FR-052 static build under a sub-path has the same pages as serve mode, including feature pages, fonts and search index`; `FR-004 D6 data-driven widths and bundled fonts work under the sub-path` (a segment and a phase-rail block have the expected non-zero widths from their `w-pct-N` classes, `document.fonts.check` confirms Geist and Instrument Serif loaded from `assets/fonts/`); `FR-052 theme, map mode, filters, reader and search work in the static build`; `FR-005 static footer shows the generation time`
      • T077Update `tests/e2e/security.spec.js`: `FR-004 SC-013 no page requests another origin` (overview, feature, document; with search opened and fonts loaded); `FR-004 no element carries a style attribute and no inline script exists` on every page type; `FR-001 document raw HTML and scripts stay text in the structured spec view, tooltips and detail panel`
      • T078Update `tests/e2e/self-counts.spec.js` (SC-004): counts on stats card, segments, sidebar, tree, map legend and feature pages match the checkbox counts of this repository's `specs/*/tasks.md`
      • T079Update `tests/e2e/scale.spec.js` (SC-007): overview and each sampled feature page load within 2 s with 50 features / 2,000 tasks, static build within 30 s
      • T080Run the full E2E suite in `chromium`, `chromium-nojs` and `chromium-mobile` locally and confirm CI passes in `firefox` and `webkit` (SC-016); fix engine-specific issues (for example the exclusive `<details name>` fallback in `src/client/feature.js`, `line-clamp` fallback, clipboard permission differences in tests)NEXT
      • T081Update `docs/usage.md` (FR-054): new layout (sidebar, icon rail, mobile menu), overview (stats card, Up next, tree order/depth/filter, warning rows), task map (modes, colors including blocked rose, tooltip, reveal, large-project modes), feature pages (tabs, rail, filters, chips, detail panel, addresses), reader (document list, contents, structured `spec.md`, Raw markdown), theme switch, search (shortcut, what is and is not searched), no-JS behavior, supported browsers (FR-055)
      • T082Update `docs/architecture.md`: new modules (`model/shares.js`, `ranks.js`, `warnings.js`, `task-files.js`; `parse/spec-structure.js`; `render/icons.js`, `taskmap.js`, `task-text.js`, `feature.js`, `reader.js`, `spec-view.js`, `search-index.js`), the browser module `init`/`save` convention and `app.js` bootstrap, site map entries (feature pages, fonts, search index, binary bodies), theme tokens with `light-dark()`, `w-pct` utilities and the CSP rationale, bundled fonts and `scripts/copy-assets.js`; check `docs/hosting.md` still matches (sub-path, fonts relative to the stylesheet)
      • T083Update `README.md` (FR-054, §VIII): new `docs/screenshot.png` of the redesigned overview (taken from the `mixed` fixture at 1440 × 900 with Playwright and committed) and a feature list naming the task map, feature pages, document reader, theme switch and search; user-facing content only
      • T084Review `CHANGELOG.md` `## [Unreleased]` for a complete, user-facing description of the redesign (layout, overview, task map, feature pages, reader, theme, search, removed header menu, changed page addresses) (§VI, research D18)
      • T085Remove dead code and leftovers (§II): unused exports from `src/render/overview.js` (`featureStatus` if superseded by `feature.status`, old grid helpers), unused CSS of 001, the `build:css` references in docs, and any 001 test that tests removed behavior; run `pnpm test`, `pnpm run test:coverage` (every new module covered) and the E2E suite
      • T086Run all quickstart.md scenarios 1–8 by hand against `mixed`, `nonstandard`, this repository and a static build under `/eye/`, and fix any discrepancy
  • 001speckit-eye — Zero-Setup Spec Kit Progress Dashboard1 warningComplete75/75
    • Phase 1Setup (Shared Infrastructure)7/7Complete
      • T001Create `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`
      • T002Create `.gitignore` with `node_modules/`, `dist/`, `test-results/`, `playwright-report/`, `coverage/`
      • T003Create `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`
      • T004Create `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`
      • T005Create `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)
      • T006Create `.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)
      • T007Create `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 2Foundational (Blocking Prerequisites)11/11Complete
      • T008Implement `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
      • T009Implement 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
      • T010Implement `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`
      • T011Implement `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/`)
      • T012Implement `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"
      • T013Implement `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)
      • T014Implement `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
      • T015Implement `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)
      • T016Implement `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`
      • T017Implement `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`
      • T018Implement `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`
    • Phase 3User Story 1 - See where the project stands at a glance (Priority: P1) 🎯 MVPP116/16Complete
      • T019Create 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`
      • T020Create 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`
      • T021Create 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`
      • T022Implement 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`
      • T023Add 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)
      • T024Add 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`
      • T025Style 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
      • T026Implement `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`)
      • T027Implement `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`
      • T028Implement 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)
      • T029Implement 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
      • T030Create `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`)
      • T031Create `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
      • T032Write `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)
      • T033Create `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`
      • T034Visual 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
    • Phase 4User Story 2 - Follow progress live during a long run (Priority: P2)P210/10Complete
      • T035Implement `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)
      • T036Implement `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
      • T037Add `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`
      • T038In `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
      • T039Implement `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`)
      • T040Wire 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
      • T041Add 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
      • T042Write `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
      • T043Cross-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
      • T044Add 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`
    • Phase 5User Story 3 - Read any artifact in two steps or fewer (Priority: P2)P28/8Complete
      • T045Create 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)`
      • T046Implement `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
      • T047Implement `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`
      • T048Add 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`
      • T049In `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`
      • T050Extend `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
      • T051Write `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
      • T052Add 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`
    • Phase 6User Story 4 - Publish a shareable snapshot from CI (Priority: P3)P37/7Complete
      • T053Implement `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/)
      • T054Static 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)
      • T055Implement 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`
      • T056Add 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)
      • T057Write `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)
      • T058Create `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`
      • T059Add `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
    • Phase 7Polish & Cross-Cutting Concerns16/16Complete
      • T060Write `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
      • T061Create `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
      • T062Write `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
      • T063Create `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)
      • T064Complete `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)
      • T065Run `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)
      • T066Verify 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
      • T067Run 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
      • T068Grep `src/` for unused exports, dead code, and TODOs and remove them; confirm `package.json` `dependencies` lists only `markdown-it` (§I, §II)
      • T069Complete `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)
      • T070Prepare 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)
      • US1See where the project stands at a glanceP14/4Complete
        • T071Grid 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)
        • T072Grid 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`)
        • T073Large-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)
        • T074Add 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`
      • US2Follow progress live during a long runP21/1Complete
        • T075Add 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`

Task map

speckit-eye — Zero-Setup Spec Kit Progress Dashboard75/75
Dashboard Redesign — Sidebar Shell, Task Map, Feature Pages, Document Reader and Themes85/86
First npm Release, Safe Release Automation, and Documentation Site with Live Project Dashboard48/51
  • Done 208
  • Open 3
  • Blocked 0
  • Next 1
  • 1 dot = 1 task