Project-specific rules and constraints extracted from ARCHITECTURE.md and project config files. Load this skill before implementing any change to understand boundaries, conventions, and constraints for this codebase.
Project-specific rules and constraints extracted from ARCHITECTURE.md and project config files. Load this skill before implementing any change to understand boundaries, conventions, and constraints for this codebase.
license
MIT
Project Guardrails
Auto-generated by /make-guardrails. Regenerate with the same command when architecture or conventions change.
Architecture Constraints
FSD one-way dependency flow: TUI layers must only import from layers below: app → widgets → features → entities → shared. A layer may never import from a layer above it.
app/ may import from any layer.
widgets/ may import from features/, entities/, shared/.
features/ may import from entities/, shared/.
entities/ may import from shared/ only.
shared/ has no internal imports from other FSD layers.
src/core/ must NOT import from src/daemon/, src/app/, or any FSD UI layer. Core is runtime-agnostic.
src/daemon/ may import from src/core/ and src/shared/ but NOT from src/app/ or FSD UI layers (widgets/features/entities).
src/client/ (CLI) consumes IPC only — it must not import daemon internals directly.
UI must use Inversify DI: Components use useInject<T>(TYPES.XxxService) instead of importing daemon functions directly. Service interfaces are in src/shared/services/types.ts, IPC implementations in src/shared/services/.
Dictionary dispatch over switch/case: Use Record<string, () => void> for command handling, not nested if/else or switch statements.
No src/tui/ or src/board/ directories — these are deleted. All TUI code lives in the FSD layers (app/, widgets/, features/, entities/, shared/).
IPC contract lives in src/types.ts — it is the single source of truth for all IPC request/response shapes (IpcRequest, IpcResponse, LoopOptions, LoopMeta).
Inversify DI container bindings in src/shared/container/index.ts — service interfaces bound to IPC implementations, not daemon internals.
File Organization
No god-files: Each file must have one clear responsibility. Do NOT create dumping-ground files like constants.ts, types.ts, config.ts, or utils.ts that collect unrelated things.
Magic numbers live in src/shared/config/constants.ts (project-scoped name).
Filesystem paths live in src/shared/config/paths.ts.
User-facing strings live in src/shared/i18n/en.json.
background-cli.test.ts is excluded from default pnpm test runs (run with pnpm test:all manually if needed).
TUI testing with ink-testing-library: Use render(), lastFrame(), stdin.write().
Wrap absolute/100% layouts in a sized <Box>.
Type chars one-at-a-time with await delay() between them.
await after stdin.write() for async key handling (esc, ctrl combos).
Test isolation: Use LOOP_CLI_HOME env var to isolate daemon state per test.
Pre-existing test failures: tests/cli.test.ts, tests/loop-controller.test.ts, and tests/projects.test.ts have known pre-existing failures. Confirm failures are pre-existing before touching assertions.
New/changed TUI components must ship with a .test.tsx even though src/tui/** is coverage-excluded.
Build & Deployment
Package manager: pnpm (version 10.x via packageManager field). Use pnpm, not npm or yarn.
Dev runner: tsx (replaces Bun). pnpm run dev runs tsx src/cli.ts.
Build: pnpm run build = tsc -p tsconfig.build.json + copy entry.js and esm-loader.js to dist/.
Node >= 20 required for CLI, daemon, and TUI.
Gate order: typecheck → lint → test → build. Always run all four before claiming done.
pnpm run typecheck = tsc --noEmit
pnpm run lint = eslint src/ tests/
pnpm run test = vitest run --exclude '**/background-cli.test.ts'
pnpm run build = tsc -p tsconfig.build.json + copy
Distribution: Published to npm as loop-task. bin field points at dist/entry.js.
Docker: node:20-slim base image, volume mount ~/.loop-cli.
Gate in agent context: Every change must pass rtk npx tsc --noEmit → rtk pnpm lint → rtk pnpm test (from openspec/config.yaml).
Data & State
All persistence is filesystem-based under ~/.loop-cli/ (overridable via LOOP_CLI_HOME).
State files: loops.json, tasks.json, projects.json — JSON arrays.
Write strategy: All state writes use writeFileAtomic() (temp-then-rename) for crash safety.
Synchronous writes to preserve immediate-disk-state-on-pause semantics.
No schema versioning: Future LoopMeta shape changes risk breaking persisted JSON. Corrupted files are silently skipped.
Chain context is ephemeral: In-memory only per loop iteration. Never persisted to disk or exposed via IPC.
Log rotation: 1 MB max per file, 3 generations (.{1}, .{2}, .{3}).
Daemon PID file (daemon.pid) and code signature (daemon.sig) recreated on each daemon start.
Security
Trust boundary: Local machine/user only. No network listener by default.
No authentication: Daemon trusts local socket connections.
No secrets handling: Loop metadata and logs are plain files. Never read or output .env files.
HTTP API is unauthenticated: Binds to 0.0.0.0 by default. Securing access is the user's responsibility at the network layer (VPN/SSH tunnel/firewall).
Chunked abortable sleep: SLEEP_CHUNK_MS = 200ms for responsive pause/resume/trigger.
opencode-onboard.json is the team config (shared, git-tracked). .opencode/opencode-onboard.user.json is user-local override (gitignored). Tier models: plan, build, fast.