Skip to content

Tasks 003 — CI wiring ​

Execution skill: superpowers:subagent-driven-development — one implementer per task, then a two-stage review (spec compliance, then code quality). superpowers:test-driven-development applies inside every task: no production code before a failing test that demands it. Reach for superpowers:systematic-debugging on any surprise rather than guessing.

Derived from plan.md (approved). Each task is small, independently verifiable, and reviewed as its own diff. Split where a reviewer could reject one task while approving its neighbour — not where the work merely changes subject. A task is done only when it satisfies the definition of done in CLAUDE.md.

Global Constraints in plan.md apply to every task and are not repeated per task.


T1 — The CI workflow, guarded by a structural test ​

Satisfies: R1, R2, R3, R4, R5, R6, R7, R8, R9, R11, P1 #1, P1 #5, P2 #1, P2 #2, P2 #3, SC4, SC5.

The test is written first and drives the workflow: it fails because .github/workflows/ci.yml does not exist, then passes once the workflow is created exactly as plan.md § Approach specifies.

  • [ ] Add the parser dependency: pnpm add -D -w yaml (root workspace). Confirm pnpm install stays non-interactive and the lockfile updates; yaml is pure JS with no build script, so strictDepBuilds is not engaged.

  • [ ] RED: create tests/ci/workflow.test.ts that reads .github/workflows/ci.yml, parses it with import { parse } from 'yaml', and asserts — one it per criterion, named for it:

    ```ts
    import { readFileSync } from 'node:fs';
    import { join } from 'node:path';
    import { fileURLToPath } from 'node:url';
    import { parse } from 'yaml';
    import { describe, expect, it } from 'vitest';
    
    const repoRoot = fileURLToPath(new URL('../..', import.meta.url));
    const workflow = parse(
      readFileSync(join(repoRoot, '.github/workflows/ci.yml'), 'utf8'),
    );
    const pkg = JSON.parse(
      readFileSync(join(repoRoot, 'package.json'), 'utf8'),
    );
    // The single job, whatever it is named.
    const job = Object.values(workflow.jobs)[0];
    const steps = job.steps as Array<Record<string, unknown>>;
    const runs = steps
      .map((s) => (typeof s.run === 'string' ? s.run : null))
      .filter((r): r is string => r !== null);
    const stepUsing = (prefix: string) =>
      steps.find(
        (s) => typeof s.uses === 'string' && s.uses.startsWith(prefix),
      );
    const withOf = (step: Record<string, unknown> | undefined) =>
      (step?.with ?? {}) as Record<string, unknown>;
    const majorOf = (spec: string) => {
      const m = /(\d+)/.exec(spec);
      return m ? Number(m[1]) : 0;
    };
    ```
    
    Cases (`SC4`/`P1 #1`): `workflow.on.pull_request` is a defined key; `workflow.on.push.branches`
    includes `'main'`; the four `pnpm` checks appear as `run` commands **in order** —
    `runs.filter(r => r.startsWith('pnpm ') && !r.includes('install'))` deep-equals
    `['pnpm lint', 'pnpm typecheck', 'pnpm test', 'pnpm docs:build']`. (`P1 #5`)
    `workflow.concurrency['cancel-in-progress']` is `true`. (`SC4`)
    `withOf(stepUsing('actions/setup-node')).cache` is `'pnpm'`. (`P2 #2`/`SC5`)
    `majorOf(String(withOf(stepUsing('actions/setup-node'))['node-version']))` is `>=`
    `majorOf(pkg.engines.node)`; and `withOf(stepUsing('pnpm/action-setup')).version` is either
    `undefined` or equal to the pnpm version parsed from `pkg.packageManager`. (`P2 #3`) a helper
    that the four check commands are all present so a removed step fails an assertion naming it.
    Run `pnpm exec vitest run tests/ci/workflow.test.ts` — watch it FAIL (file missing).
    
  • [ ] GREEN: create .github/workflows/ci.yml exactly as in plan.md § Approach (name CI; on:pull_request + push.branches: [main]; concurrency with cancel-in-progress: true; one check job on ubuntu-latest; steps actions/checkout@v4 → pnpm/action-setup@v6 (no version) → actions/setup-node@v4 with node-version: '22' and cache: 'pnpm' → pnpm install --frozen-lockfile → pnpm lint → pnpm typecheck → pnpm test → pnpm docs:build). Pin each action to the current major at implementation time. Rerun — PASS.

  • [ ] REFACTOR: none expected.

  • [ ] Confirm the test has teeth — reorder two checks in ci.yml (e.g. test before typecheck), watch the ordered-checks case fail; delete the concurrency block, watch that case fail; restore both.

  • [ ] pnpm typecheck exits 0 (the new test typechecks, no any, no !); pnpm lint exits 0 (pnpm format if needed); pnpm test green.

  • [ ] Commit (ci: add GitHub Actions workflow and its structural guard), including the updated pnpm-lock.yaml so --frozen-lockfile will pass on the runner.

Verified by: tests/ci/workflow.test.ts (all cases); pnpm typecheck/pnpm lint/pnpm test green.


T2 — Document the enforcement rule and the CI facts ​

Satisfies: R10, R12; creates the dated-verification home the observational criteria (SC1–SC3, SC6, P1 #2–#4, P3) are recorded in at step 5.

Docs, not code, so the RED step guards existence and linkage rather than behaviour.

  • [ ] RED: add to tests/ci/workflow.test.ts a describe('the CI enforcement is documented') with a case asserting docs/architecture/ci.md exists, contains a heading mentioning branch protection, and that CLAUDE.md references docs/architecture/ci.md. Run — watch it FAIL (doc missing).
  • [ ] GREEN: create docs/architecture/ci.md capturing: - The workflow — the shape from plan.md (two triggers, one fail-fast job, four checks, pnpm cache, concurrency), in prose a future reader can act on. - Branch protection (R10) — the exact rule on main: require the check job's status check to pass before merging (Settings → Branches → add rule for main, or gh api -X PUT repos/:owner/:repo/branches/main/protection … with the required check). State plainly why it is not a committed artifact (a GitHub-side setting) and that it is set by the human once and verified by hand. - Standing guards — strictDepBuilds: true means any future build-script dependency must be added to allowBuilds or CI's install fails (research.md V1); mise will own a single Node version for every environment later, at which point CI reads that pin instead of 22 (research.md V3). - A "Verification log" section with placeholder rows for the dated observations filled at step 5 (SC1–SC3, SC6). Add a line to CLAUDE.md's Reference section linking docs/architecture/ci.md. Rerun — PASS.
  • [ ] Confirm teeth — remove the CLAUDE.md link, watch the case fail, restore.
  • [ ] pnpm test green (confirm the CLAUDE.md edit did not break constitution.test.ts).
  • [ ] Commit (docs: capture CI shape and the manual branch-protection rule).

Verified by: tests/ci/workflow.test.ts › the CI enforcement is documented; pnpm test green.


T3 — Fill traceability and lock SC8 ​

Satisfies: SC8, SC7; final in-repo verification.

  • [ ] RED: add to tests/ci/workflow.test.ts a case SC8: the spec 003 traceability table is complete and its named tests exist that parses specs/003-ci-wiring/spec.md's traceability table (rows | criterion | … |), asserts no cell is empty or a (…to be named…) placeholder, and that every backtick-quoted test name in a cell is found as a string in some file under tests/. Manual-record cells (no backticks) are allowed as notes. Run — watch it FAIL (cells still say "to be named").
  • [ ] GREEN: fill the Test/verification column of spec.md's traceability table from plan.md § Test strategy — automated rows cite `tests/ci/workflow.test.ts` (with the specific case name), observational rows cite the dated record in docs/architecture/ci.md. Rerun — PASS.
  • [ ] Confirm teeth — blank one table cell, watch the case fail, restore.
  • [ ] Full in-repo verification: pnpm typecheck exits 0; pnpm lint exits 0; pnpm test fully green including tests/workflow/* (specs 001/002 intact — SC7); find docs/superpowers -type f | wc -l prints 0 (R12/SC7).
  • [ ] Commit (specs: fill spec 003 traceability and lock SC8).

Verified by: tests/ci/workflow.test.ts › SC8: …; clean pnpm typecheck/pnpm lint/pnpm test; zero files under docs/superpowers/.


Notes ​

Staging area for decisions and surprises found during implementation — including anything that turned out differently from what plan.md assumed. Move each one into spec.md, research.md, or docs/ before closing the feature; this section is not a home.

  • Environment quirk (this sandbox): node/pnpm are not on the default PATH; they live at /opt/homebrew/bin. Prefix export PATH="/opt/homebrew/bin:$PATH" when a command reports command not found.
  • After each task, run pnpm format before committing so pnpm lint stays green throughout.
  • Verification at step 5 (not a coding task — the observational criteria). These are proven only on the real run and in GitHub's settings, and recorded with their dates in docs/architecture/ci.md:
    • SC1/P1 #2 — open the 003 PR with one check deliberately broken; confirm a red run whose failed step names the check, and that later steps did not run. Then fix and confirm green.
    • SC2/P1 #3 — with every check passing, confirm a green run.
    • SC3/P1 #4 — after merge, confirm the push:main run reports a status on the commit.
    • SC6/P3 — the human sets the branch-protection rule on main (per ci.md), then confirms a red/pending PR cannot be merged and a green one can (R10).
    • Record each with its date; the traceability rows for these cite that log.
  • Graduation (step 6): research.md § Graduation lists the CI facts to consolidate into docs/architecture/ci.md — much of it lands in T2; confirm nothing is left only in research.md before closing the branch (superpowers:finishing-a-development-branch).
  • Isolation (step 3.5): implement in a worktree (superpowers:using-git-worktrees) so a long run cannot disturb the working tree, per the constitution.