# Claude Code — Dev Guide for singularity-forge See [AGENTS.md](AGENTS.md) for SF planning conventions and the promote-only state rule. The foundational product contract is [ADR-0000: SF Is a Purpose-to-Software Compiler](docs/adr/0000-purpose-to-software-compiler.md). ## Build pipeline (MUST READ before editing extension source) Source TypeScript files under `src/resources/extensions/sf/` are **not loaded directly at runtime**. The loader (`src/loader.ts`) resolves extension entry points from `dist/resources/extensions/sf/` (compiled `.js`) and copies them to `~/.sf/agent/extensions/sf/` via `initResources`. Editing a `.ts` source file has **no effect** until you recompile: ```bash npm run copy-resources # tsc --project tsconfig.resources.json + file copy ``` This clears and rebuilds `dist/resources/` in one shot. Expect ~60–90 s on first run; subsequent runs reuse tsc's incremental cache if you keep one. The `dist-redirect.mjs` resolver (used by tests and `dev-cli.js`) only redirects `.js → .ts` for imports whose `parentURL` is inside `/src/`. Files loaded from `~/.sf/agent/extensions/sf/` (compiled JS) are **not** redirected. ## Running tests **Use vitest — no pre-compilation step needed.** ```bash # Run a specific test file (fast, no coverage overhead): npx vitest run src/resources/extensions/sf/tests/.test.ts --config vitest.config.ts # Run the full SF extension test suite: npm run test:unit # Run only tests affected by recent changes (fast feedback loop): npx vitest run --changed --config vitest.config.ts # Watch mode for active development: npx vitest --config vitest.config.ts ``` **Do not use Python for one-off JSON/hash work.** The resource fingerprint in `~/.sf/agent/managed-resources.json` is computed by Node's SHA-256 — Python's `hashlib` produces a different result for the same files, which breaks the fast-path check in `initResources` and causes a 30-60 s full resync on every launch. Use `node -e` (or `jq`) for any shell-level JSON/hash operations in this repo. ## Lint: test-import-drift guard **Problem:** Test files with itemized `import { foo } from "module"` and many named imports (≥6) carry a maintenance trap — adding a new `describe(...)` block that uses a module function without updating the import list causes `ReferenceError` at vitest run-time, not at lint time. Biome's ESM lint does not catch `used identifier not declared`. **Guard:** `npm run check:test-imports` runs `scripts/check-test-imports.mjs` which statically scans all `*.test.{js,mjs,ts}` files for this anti-pattern. It flags files that have ≥6 itemized imports AND reference a camelCase identifier not in the import list. False positives for test locals, vitest globals, Node builtins, and keyword-like words are filtered. The check is NOT integrated into `npm run lint` by default (too broad for the current threshold) but runs as `npm run check:test-imports`. Add it to the `lint` script if the threshold is later lowered. **Convention:** Test files whose subject is the public surface of a SF module (migration tests, integration tests over a module's full API) should use `import * as from ""` instead of itemized imports when ≥6 named members are needed. This avoids the maintenance trap entirely. The check script targets files with ≥6 itemized imports + an undeclared camelCase identifier. Known false-positive categories are filtered (test locals, vitest globals, Node builtins, keyword-like words, boolean flags), but some legitimate cases may still appear in complex test files — use judgement when triaging output. ## Key directories | Path | Purpose | |------|---------| | `src/resources/extensions/sf/` | Extension TypeScript source (edit here) | | `dist/resources/extensions/sf/` | Compiled output (rebuilt by `copy-resources`) | | `~/.sf/agent/extensions/sf/` | Installed copy (synced from dist on startup) | | `src/resources/extensions/sf/prompts/` | Prompt templates (`.md`) | | `src/resources/extensions/sf/tests/dist-redirect.mjs` | Module resolver hook for tests | ## Template variables When adding a new `{{variable}}` to a prompt template in `prompts/`, you must: 1. Pass it in every `loadPrompt("template-name", { ..., newVar })` call site (`auto-prompts.ts` is the main one for execute-task). 2. Add it (with a sensible placeholder value) to any test that calls `loadPrompt("template-name", {...})` — see `src/resources/extensions/sf/tests/plan-slice-prompt.test.ts`. 3. Run `npm run copy-resources` to land the change in dist. `loadPrompt` throws at runtime if any `{{var}}` in the template has no corresponding key in the vars object — this is intentional to catch template/code drift early.