Skip to main content
ClaudeWave
Skill1.9k repo starsupdated 3d ago

penguin-harness-dev

Use when developing PenguinHarness itself — changing packages/{core,server,web,cli,desktop,landing,docs,skills}, the built-in model catalog, the installers or the release workflow; writing or auditing changelog entries; writing a blog post or capturing release screenshots; deciding what to do about data already on disk; or auditing prose that reads like a leaked authoring session. Covers the two-repo symlink layout, the CI-parity verification chain, the record-and-ship contract, where blog media is hosted, and the seams that are intentional.

Install in Claude Code
Copy
git clone --depth 1 https://github.com/Prism-Shadow/penguin-harness /tmp/penguin-harness-dev && cp -r /tmp/penguin-harness-dev/.agents/skills/penguin-harness-dev ~/.claude/skills/penguin-harness-dev
Then start a new Claude Code session; the skill loads automatically.

SKILL.md

# Developing PenguinHarness

PenguinHarness is a TypeScript monorepo: an Agent SDK (`packages/core`), an HTTP server (`packages/server`), a Web App (`packages/web`), a CLI (`packages/cli`), an Electron shell (`packages/desktop`), the landing and docs sites, and the shipped skill library (`packages/skills`). It **consumes** LLM providers through `@prismshadow/agenthub` and implements no provider clients of its own.

## Repo shape — read before your first edit

Two repositories sit side by side: the implementation repo (`penguin-harness`) and the design repo (`penguin-harness-design`). Three paths inside the implementation repo are symlinks into the design repo and are gitignored here:

- `design/` → the design repo, so specs are reachable as `design/specs/…`
- `AGENTS.md` → `design/AGENTS.md`
- `CLAUDE.md` → `AGENTS.md`

Editing `AGENTS.md`, `CLAUDE.md` or anything under `specs/` edits **the design repo's files**. Commit them there, on their own branch, in their own PR. They can never appear in an implementation-repo PR. A fresh clone has none of the three links; recreate them by hand.

Both repos take changes through branch + PR against `main`, squash-merged. Do not push to `main`.

Branches are `feat/<topic>`, `fix/<topic>`, `docs/<topic>` in both repos. Some older branches here read `docs-<topic>` instead: a remote branch literally named `docs` once held that ref namespace and made the slashed form unpushable. It is gone — if a push is ever rejected as a directory/file conflict again, `git ls-remote --heads origin` names the branch responsible.

Independent changes each get their own git worktree under `../penguin-harness-wt/<topic>/` so several can run in parallel.

## Verify what you changed

Node must be >= 24. In each worktree, `pnpm install --frozen-lockfile` first.

The full chain CI runs is `pnpm build`, `pnpm format:check`, `pnpm typecheck`, `pnpm test`, `sh scripts/test-installer.sh`. **Do not reach for `pnpm test` by reflex** — it is nine packages and ~2500 tests, minutes per run, and for a docs-only diff it proves nothing the narrow run does not. Pick the narrowest evidence that would actually fail for this change's regression:

```sh
pnpm --filter @prismshadow/penguin-core exec vitest run test/model-catalog.test.ts   # one file, ~1s
pnpm --filter @prismshadow/penguin-web test                                          # one package
```

| Changed | Run |
| --- | --- |
| Markdown only — changelog, `.github/CONTRIBUTING.md`, `.agents/`, README | `pnpm format:check` |
| `packages/docs/content/**`, blog posts under `packages/landing/content/**` | `format:check` + the `docs` and `landing` package tests (search index, blog fixtures) |
| `packages/skills/skills/**` | the `skills` package test + `docs`'s `skills-sync.test.ts` |
| The model catalog | core `model-catalog.test.ts`, web `model-grouping.test.ts` and `protocol-path.test.ts`, server `models.test.ts` |
| One package's source | that package's `test`, plus `typecheck` |
| Exported core types, or anything downstream imports | `pnpm build` + `pnpm typecheck` before any test |
| `package.json`, the lockfile, `pnpm-workspace.yaml` | `pnpm install --frozen-lockfile` + `pnpm build` |
| Installers, `release.yml` | `sh scripts/test-installer.sh` |

Always cheap, always worth it: `git diff --check`, and `pnpm format` + `format:check` on any diff at all.

Run the whole chain in exactly three cases: the change spans the repo widely enough that nothing narrower is credible, you are diagnosing a CI failure, or you are asked to. There are no coverage thresholds in this repo, so nothing forces a wider run than the behavior needs.

`ci-windows` runs the same minus `format:check`, plus `scripts/test-installer.ps1`. Two failures there are known and are not your diff: a bare `Failed` worker crash, and an `environment.test.ts` truncation-timing assertion — rerun before debugging. An `EBUSY` on removing a directory that is some child process's cwd is real, not a flake.

**A fake that is kinder than the real thing hides the bug it was written for.** Transports are faked at a seam here — `createSocket`, `createClient`, a stubbed `globalThis.fetch` — and the half that drifts is the failure contract, not the happy path. A fake that hands back a fresh cursor where the adapter returns the one it was given, or that resolves where production parks, passes a test whose subject is exactly that path. Write the fake's failure branches from the adapter's, not from what the assertions need.

**A structural assertion that was already true at the base commit is not a test.** Reading source text in a test is house style here (20 web test files do it), which makes it easy to assert a shape the file already had. Replay each new assertion against `git show <base>:<path>` and keep the ones that fail there.

The local server suite drops a few tests under full parallel load — they time out and pass on their own. Rerun the file alone before blaming the diff.

Once the evidence you chose passes, commit and push to the current branch without asking. Force-pushes and reverting someone else's commits still need confirmation.

**On a conflicting PR, waiting for CI is waiting for nothing.** The workflows run against the merge commit GitHub builds from the branch and `main`, and it does not build one while the two conflict — the checks never start, so polling `statusCheckRollup` returns the same pending or empty list forever. Read the state before the checks: `gh pr view <n> --json mergeable,mergeStateStatus` answers `CONFLICTING` / `DIRTY`. Merge `origin/main` into the branch, resolve, push, and only then expect checks. Two more things that pass silently and are worth checking in the same pass: an auto-merged file is not a correct file, so read the merged result where two PRs touched one region; and a test constant pinned to a count the other PR changed fails for a reason that has nothing to do with either change — derive the count instead of re-pinning it.

## Record and shi