Naming follows a **(domain, leaf) derivation** model.
Rules
Apps Readme
3When editing an `apps/*/README.md`, MUST have YAML frontmatter with these required fields: `name` (human-readable display name), `description` (1–3 sentences; use `>-` for multiline preservation), `schedule` (freeform timing string e.g.
When authoring an `apps/*/README.md` from scratch, MUST follow this canonical frontmatter scaffold (absorbed from the deleted `.claude/templates/app-readme.md`): ```yaml --- name: App Display Name description: >- 1-3 sentence description of what the app does.
Apps Scripts
1When generating AI text in an apps/ script, MUST use the cross-provider router; MUST NOT call LLM APIs directly.
Behavior Platform Contracts
2Before tuning a config to a rate limit, quota, or API constraint from a third-party doc, MUST run one empirical probe to confirm the actual observed limit; MUST NOT trust documented limits as observed behavior.
When applying a fix that depends on a managed platform honoring a config setting (Windmill CE `concurrent_limit`, Cloudflare KV limits, etc.), MUST empirically verify before shipping (fire the limit, observe enforcement); MUST NOT trust an unverified platform contract.
Behavior Tests
1Before declaring a new test complete, MUST verify it fails on the known-regressed state (revert the fix, run the test, confirm failure, re-apply the fix); MUST NOT rely on "the test passes" as sufficient evidence.
Behavior
8When implementing any new computation or flow, MUST enumerate expected behavior at 0, 1, empty, max, and error inputs.
When the diagnosed design flaw is a **class-level mechanism** (a shared pattern across siblings, an orchestrator family, a repeated anti-pattern — anything affecting more than one file/script), MUST land every instance in the repo in the same session.
A session that produced a new app or a major change to an existing app (new file under `apps/**/` at any nesting depth — including domain-grouped `apps/{domain}/{name}/` — or >50 lines added/modified across `apps/**/*.ts` ∪ `integrations/**/*.ts`) MUST be machine-reviewed by `/implement-audit` on **the vendor the `adversarial-review` policy row selects — a different vendor from the author**, BEFORE the change lands.
When a task has an authoritative deterministic source (REST API, web scrape with a stable HTML anchor, computation, lookup table, repo file), MUST use that source instead of an AI call; MUST NOT substitute AI inference for data that can be fetched, computed, or parsed.
Pre-send completion test: before ending ANY turn, every task surfaced this turn MUST be either (a) DONE now, or (b) explicitly blocked on the user with the blocker named.
MUST NOT build hooks, checks, or rules for failure modes that have not yet occurred.
When planning any non-trivial work, MUST address all four before starting implementation: downstream consumers (grep for users), peer consistency (find sibling files doing similar), edge cases (enumerate), doc surface (which docs drift).
Every work product produced in a turn MUST terminate in one of two destinations: (a) printed directly in the assistant response to the user, or (b) a file inside the context repo.
Compose Files
1After editing an `integrations/*/compose*.yml` file, MUST deploy it in the same session via `integrations/komodo/deploy-stack.sh <service>`; MUST NOT expect the compose change to apply automatically.
Data Reliability
17At request/SSR time (Astro `.astro` frontmatter, API routes, middleware, any per-request handler), MUST NOT `await` a KV / DB / `fetch` read inside a loop over a key list or a collection — that is O(N) sequential round-trips and is the recurring slow-page bug.
Business logic MUST live in TypeScript independent of orchestrator (Hatchet handles scheduling/concurrency/retries; the orchestrator is infrastructure); MUST NOT couple business logic to orchestrator APIs beyond the platform-provided primitives (`task.run`, `step` outputs, `concurrency` config).
When a piece of metadata describes a data product (SLA, data sources, frontmatter), MUST co-locate it with the code that produces the product (script header for writer SLA; `pageInfo` for reader data sources; README frontmatter for app); MUST NOT create standalone inventory files.
[Folded into @rule:co-locate-metadata on 2026-05-13 — `derive-inventories-from-code` lived here from 2026-03-28 to 2026-05-13] [2026-05-13]
`validateOutcome` MUST check correctness (expected fields non-null, arrays non-empty when expected), not just existence; warnings MUST be treated as failures; MUST NOT display zeros or blanks from missing data as real values.
Every automation MUST be idempotent — running twice yields the same result; MUST NOT rely on append-only or increment-only side effects; state is derived from inputs, not accumulated.
[Folded into @rule:no-mrr-clients-duplication on 2026-05-13 — `new-client-data-goes-to-extension` lived here from 2026-03-28 to 2026-05-13] [2026-05-13]
When adding client-level data used by all three WSP portals, MUST add to the `mrr-sync` D1 database's `clients` table (`mrr:clients` in the FINANCE_DATA KV namespace until 2026-09-10); when data is portal-specific, MUST add to that portal's own store joined by codename; MUST NOT duplicate the client book's fields (codename, clientName, type, amount, dates, users, atRisk flags) into a portal's own table.
Every stored data product MUST have exactly one script that owns writes and declares an SLA; MUST NOT introduce one whose owner or SLA is ambiguous.
When a fetch returns garbage (schema mismatch, missing required fields, Zod validation fails), MUST refuse to write and keep the last good value with a staleness warning; MUST NOT overwrite good data with bad.
Every **timer-SLA** data product MUST declare a hard TTL (typically 2-3× the SLA); after TTL, DataHealth MUST show "missing" not "stale"; MUST NOT display last month's financials as merely "stale." [2026-03-28]
CF Pages build command MUST run `astro check` (or `tsc --noEmit`) then `astro build`; MUST NOT deploy on type errors; MUST NOT skip the check.
Every external API access MUST go through a typed client co-located with the fetch script; MUST NOT use raw `fetch()` for external APIs against known integration hosts (`hosts:` frontmatter from `integrations/*/README.md`).
Production code MUST be TypeScript; MAY use shell ONLY for thin wrappers around other tools (deploy scripts, hook glue); MUST NOT implement business logic in shell.
For every external fetch, MUST assert at least one domain invariant before using the data (date range overlaps the request, result count ≥ expected floor, entity types match the query); MUST NOT trust a valid schema as sufficient — valid-but-wrong data is silent corruption.
[Folded into @rule:one-owner-one-sla-per-key on 2026-05-13 — `writer-header-declares-products` lived here from 2026-03-28 to 2026-05-13] [2026-05-13]
Every read and write boundary for external data MUST validate via Zod; MUST NOT accept or emit external data with unvalidated shape; schema drift MUST break the build, not the runtime.
Feedback
18I write INTERNAL notes only.
When solving a problem requires modifying shared infrastructure on any host other than the repo working tree — host networking (`ip link/addr/route`, NetworkManager, systemd-networkd), daemon configs (`/etc/systemd/...`), creating or enabling systemd unit files outside `~/.config/systemd/user/`, modifying root-owned credentials (`/root/.docker/config.json`, `/root/.ssh/`, `/etc/op/...`), or anything that persists across reboots on a shared host (Hermes, the WSP portal hosts, MNK servers) — MUST stop and `AskUserQuestion` BEFORE executing, even if the change is "small," "additive," "documented workaround," "reversible," or "the standard fix." Diagnose freely (read-only).
Each create / edit / write under `.claude/` is a separate user approval prompt.
When implementing, deploy is part of the work: an already-authorized deploy MUST NOT be offered as a follow-up or a pending item.
MUST NOT present a fix that stops the symptom while the thing that caused it stays live.
ONCE, at the START of a task and before work begins, MUST present two plain-English sections — **Goal:** (one sentence to a few bullets, sized to the work) and **Simplest mechanism:** (the lightest way to achieve it) — then MUST wait for explicit approval.
When the user asks "which would you pick?", "what's best?", or any other question seeking a single recommendation, MUST reason from greenfield: ignore sunk cost, migration cost, and Claude's own work cost — the user does not bear those.
Hudu is read-only for me.
Before asserting Claude lacks access to any external service, system, or data source ("I can't access X", "the agent couldn't access X"), MUST check three sources in order: (a) `integrations/<service>/README.md` exists — follow its access pattern, but a capability LIMIT written in one ("can't be scripted", "no API", "only via the CLI") is a CLAIM and never a reading: obey it only where it names what was probed and when, else re-probe before repeating it; (b) `op item list --vault "AI Context" | grep -i <service>` matches — creds are stored; (c) the service appears in a Komodo stack list if containerized — access flows through `integrations/komodo/`.
Ashkaan's city, ZIP and mailing address live in `knowledge/people/ashkaan-hassan/README.md § Identity`, which is always-loaded via the `.claude/CLAUDE.md` import; his timezone is America/Los_Angeles.
When the user has explicitly authorized an action this turn (verbally, via `AskUserQuestion`, or a "do it" / "yes" / "I approve" directive), MUST execute it end-to-end myself.
**The recognizable moment is the SOURCE about to be cited.** Each of these is a CLAIM, never a reading — swap it for the thing on the right before asserting from it: a repo doc / README / SPEC → what it describes · anything said in this conversation → the artifact itself · my own earlier write-up this session → the original, re-read · a reviewer's or subagent's finding → the `file:line` it cites · a function or constant's NAME → its body · code near the line → the line · a model's summary of a page → the page · one commit message → the tree · **a tool result → what that tool actually measures**, which may be a different question, answered stably and wrongly.
MUST NOT add fmt, lint, or similar auto-quality enforcement that covers `.md` files in hooks, scripts, or CI; markdown is not code.
When executing a `systemctl restart|stop|disable` (or any process-killing action) against a service whose process tree contains the current Claude session's runtime, MUST stop and ask the user — killing it terminates the session issuing the command.
When verifying or QA'ing a change to a CF-Access-gated portal (finance / operations / sales / dashboard), MUST verify against the production D1 database behind the page (`createD1` / the D1 query API) or screenshot the live gated page with the dashboard-probe service token, per `integrations/cloudflare/README.md § "Verifying a portal feature"`; MUST NOT conclude a change is "unverifiable locally" or "needs deploy first" because the page is auth-gated — a CF-Access gate is a login wall for my browser, never a data wall.
When building or migrating an automation that observes or acts on an external system, MUST research what each target exposes (cross-network API / push-webhook / SSH-only) BEFORE picking a transport, and MUST prefer the cross-network API (HTTP/REST/S3) over SSH whenever one exists AND it does not lengthen the runner→target path; MUST NOT default to SSH without checking.
When QA'ing visual UI output (rendered slides, screenshots, page renders, design candidates, portal pages) before shipping a change to a deployed surface, MUST dispatch a fresh-context subagent for the visual review; MUST NOT inline-QA by reading PNGs in the main orchestrator context.
Default to the simplest mechanism that works.
Google Urls
1Google URLs (Sheets, Docs, Drive, Calendar, Gmail, Contacts) require auth and always fail WebFetch with 401/403; MUST NOT use WebFetch on them, and MUST go through `integrations/google/` instead.
Governance Apps
5When an app's SPEC declares `ai_judgment_features:` (non-empty list in YAML frontmatter), MUST include `apps/{name}/evals/eval.ts` + at least one fixture file (`apps/{name}/evals/fixtures/*.json`); MUST NOT ship a new AI-judgment app or feature without an eval.
When adding a major feature to an existing app under `apps/{name}/`, MUST author the build SPEC at `projects/{domain}/{YYYY-MM-DD}_{slug}/{name}.spec.md` with the unified 13-section schema before any implementation code is written; the project folder persists as long-term documentation.
When creating a new app under `apps/{name}/` whose implementation includes `.ts`/`.tsx` files (automation, Windmill script, edge worker, etc.), MUST include `apps/{name}/SPEC.md` with the 12-section schema (mirror `integrations/trigger-dev/new-app-guide.md § SPEC.md` for the canonical template), covering all 11 base sections: (0) user's ask verbatim, (0a) how we interpreted this, (0b) simplest shape that could work, (1) behavior contract, (2) input contract (trigger, schedule, params, credentials), (3) output contract (D1 writes with row type + SLA, stdout shape, side effects), (4) boundary cases at 0 / 1 / empty / max / error, (5) one acceptance command with inline expected stdout, (6) test plan pointer, (7) data-reliability checklist, (8) failure modes — plus section 9 (AI Eval Plan) when the SPEC declares `ai_judgment_features:` non-empty in YAML frontmatter (per `@rule:apps-eval-required-for-ai`).
When creating a new app under `apps/{name}/` that adds `.ts`/`.tsx` code (beyond test files themselves), MUST include at least one paired test file exercising the acceptance behavior (`apps/{name}/*.test.ts` or `apps/{name}/tests/*.ts`).
MUST NOT send email directly from arbitrary code.
Governance Integrations
2Writes to the context repo from automation code (apps/, integrations/) MUST go through `integrations/github/git_local.ts` (`commitViaSsh` — SSH to the repo host, commit in a per-automation worktree) — NOT through the GitHub REST API.
When writing or modifying a typed API client for an external integration, MUST place the canonical reference at `integrations/{name}/{name}.ts`; Daedalus-side code (apps, `projects/**/*.ts`) MUST import the reference directly via filesystem relative path.
Governance Projects
4The shard model is **opt-in**, for projects deliberately split into ≥2 independent parts that can be pursued in parallel.
When a project's status is `blocked`, MUST use `blocked-on:` frontmatter field naming what's blocking; MUST NOT use `next:` for blocked projects.
A project may be marked `completed` (or `monitor`) autonomously in exactly one place: `/close` step-2.1, and only when its gate-1 script reports no un-reported SPEC, no open shard row, and no unchecked `## Next Steps` box, AND the goal is plainly met — the judgment defaults to leaving the project active.
When creating a new project, MUST place at `projects/{domain}/YYYY-MM-DD_name/` with `README.md` containing required frontmatter (project, status, created, tags); MUST push immediately after creation.
Governance Readme
1When adding or editing a markdown table in a README whose first column lists files or subdirectories of the README's own directory, MUST either (a) auto-generate it via Phase A with a named reader or (b) delete it and replace with narrative; MUST NOT hand-maintain a file-listing table — drift is inevitable.
Governance
5When my own tools (curl, fetch, headless screenshot) cannot access a user-protected resource (CF Access, SSO, VPN, browser auth, IP allowlist), MUST NOT assume the user also cannot access it; MUST NOT escalate to a more-public mechanism (remove the auth gate, publish to a no-auth URL, upload to a public image host, route via a separate unauthenticated CDN) to solve my OWN tool-access problem.
Every git commit subject MUST begin with an actionable verb (add / fix / update / refactor / remove / etc.), ≤100 chars, no prefix-only-no-content shapes; MUST NOT use vague subjects ("wip without scope", "fix it", multi-line streams).
Before writing "shipped", "deployed", "live", "auto-deploys", or any deploy-command instruction in session output (close summary, implement-audit report, plan file, commit message), MUST consult the relevant `integrations/<name>/README.md` for the repo's actual deploy model and verify the claim against it; MUST NOT treat repo-local convenience scripts (`package.json` `deploy`, `Makefile` `deploy`, etc.) as authoritative when the integration README documents a different model.
When a lint or format error occurs, MUST fix it at the source; MUST NOT add the file to an exclude list to avoid the fix.
When the user asks to end the session in any words — "close", "wrap up", "close this damned session" — MUST invoke `/close` (journal entry + commit + push); MUST NOT hand-roll it with a direct `journal/<date>.md` edit, a bare `git commit`, or a `worktree-merge.sh` call, which lands the change while dropping the journal frontmatter, the project-README update, `/close`'s § 2.5 judgment checks and the `implement-audit:` trailer.
Health Data
1Health data MUST be stored in year-based files: `knowledge/health/{topic}/{year}.md`; MUST load current year by default; MUST NOT always-load historical years — pull only on-demand for trend analysis.
Integrations
2A folder under `apps/` holds repo-authored code — anything we wrote.
A folder under `integrations/` MUST wrap an external product — third-party SaaS, OSS running on another host, vendor API, or a host-bound product we connect to.
Journal
4Every session block MUST start with `**Action:** <verb>` where `<verb>` is one of the closed set: `completed | progressed | scaffolded | fixed | investigated | planned | shipped | migrated | removed | updated`; MUST NOT omit, replace, or use a verb outside the enum.
When editing a journal entry, frontmatter MUST contain `date` (YYYY-MM-DD) and `tags` (list); MUST contain a `sessions:` block listing one `- slug: <heading>` entry per session worked that day (parsed by `.claude/hooks/telemetry/local_aggregator.ts`); MAY contain additional structured telemetry fields per @rule:journal-session-frontmatter; MUST NOT add unrelated ad-hoc frontmatter fields.
Journal entries MUST be structured (headings, bullets, labeled sections like Changes/Lessons/Blocked/Next); MUST NOT contain narrative prose, code diffs, or deploy play-by-play.
Each session in a journal MUST use a `###` heading matching one of two shapes: `one-off (<brief description>)` OR `<domain>/<YYYY-MM-DD>_<slug>` (optionally followed by ` (<parenthetical>)`, ` — <em-dash-tail>`, `/<sub-slug>`, or `_<sub-slug>`); MUST NOT use `##` or freeform section names.
Meta · Adversarial Tools
6Adversarial tools MUST treat "zero findings" as a permitted output and report it explicitly; MUST NOT pad output with trivia or speculative items to avoid the empty-result case; linter flags outputs containing only `speculative` verdicts as probable padding.
The fix loop MUST end on the WORK, and the work is measured by WHERE findings land, never by how many arrive.
Adversarial tools MUST operate only on changes in the current conversation or the named diff range; MUST NOT hold the patch on, or pad findings with, unrelated repo state or other sessions' work.
Adversarial-tool output MUST include a structured section (YAML fence with `findings: [{id, verdict, rule, message}]`) consumable by the telemetry aggregator, in addition to any human-facing prose; MUST NOT emit prose-only output.
Adversarial-tool output MUST emit every finding with a triage verdict: `fix-now | deferred-batch-N | speculative | out-of-scope`; MUST NOT emit free-form "here's a list of findings" output — untriaged findings produce the fix-every-finding audit loop.
Any adversarial or review tool (`/implement-audit`, `/review`, `/security-review`) MUST emit triaged findings (`fix-now | deferred-batch-N | speculative | out-of-scope`) AND end the fix loop per `@rule:adversarial-recursion-cap` (a round with no distinct new defect); MUST NOT loop indefinitely.
Meta · Ai Layer Authoring
12When an agent runs work Ashkaan is not present to supervise, each of its hard limits MUST be enforced by withholding the tool that would breach it — scope the agent's `tools:` list — and MUST NOT rest on an instruction in its brief telling it not to.
Before authoring ANY new rule, hook, or check, MUST first establish whether Claude Code already exposes the behavior natively — a `settings.json` key, a lifecycle hook event, or artifact frontmatter — and MUST use that surface when it exists.
Every behavioral rule MUST either run automatically (hook, linter) OR fire as a concrete tool call at a named decision point inside a skill step graph OR carry a `<!-- judgment-core: <reason> -->` marker per `@rule:rule-default-deterministic-trigger` justifying why neither is possible; MUST NOT be cited as enforcement if it exists only as advisory prose.
When authoring or dispatching a `.claude/` artifact that does LLM work whose work-type MATCHES a row in `workbench's apps/ai/prompt-router/policy.ts` (machine-readable mirror `policy.json`), the model MUST be the row's assignment; MUST NOT be chosen at the call site.
When multiple surfaces constrain the same actor for the same action, MUST consolidate to the most powerful surface (**harness config > hook > app > rule** for enforcement); the mechanism's header comments are the declaration.
When creating OR amending any `## <slug>` section in `.claude/rules/**`, MUST run it through `/author` (`rule` branch for a new rule, `rule --compress` for an existing one) and MUST NOT hand-edit the body directly; the commit carries a `rule-author:` trailer naming the efficacy-gate outcome.
When a rule is violated on 3+ DISTINCT DAYS despite being in the right file, MUST promote it to enforcement (hook, linter); MUST NOT leave such a rule as prose-only.
For "When X then Y" automation placement, MUST pick surface by what X is: a `git commit` (any committer — Claude, scripts, or trigger.dev SSH) → a **native git hook** under `.githooks/` per `@rule:tiebreaker-commit-hook-placement` (thin entrypoint → body in `.claude/hooks/bodies/`); any other Claude tool call → `.claude/hooks/` PreToolUse/PostToolUse with body scripts in `.claude/hooks/bodies/`; clock tick (declarative verification, scheduled checks, anything else) → trigger.dev (a scheduled `schedules.task`).
For "Always think Y when reasoning about Z" guidance placement, MUST pick by scope of Z, DEFAULTING to the lightest deterministic trigger and treating always-loaded as the justified exception (per `@rule:rule-default-deterministic-trigger` + `@rule:no-model-judgment-jit`): mechanizable convention → a hook/linter; convention that only matters editing certain files → `.claude/rules/{topic}.md` with `paths:` frontmatter (path-scoped); task-typed guidance reliably signaled by a word that IS its activation condition → keyword-injected via `.claude/hooks/integration-match.sh`; judgment-core Claude behavior that applies every session AND cannot be checked or path-scoped → `.claude/rules/{topic}.md` with no `paths:` (always-loaded) AND a `<!-- judgment-core: <reason> -->` marker per `check-rule-minimal-context.sh`; persistent fact about a person → `knowledge/people/{name}/` (with one exception that changes the cost, not the routing: `knowledge/people/ashkaan-hassan/README.md` is itself always-loaded since 2026-08-30, because `.claude/CLAUDE.md` `@import`s it — so a fact placed in THAT file is in every prompt and is size-capped by `check-claude-md-imports.sh`; his other files there are not imported); domain-specific → that domain's README; multi-step named operation → `.claude/skills/{name}/SKILL.md`.
Every automation MUST emit at least one signal channel (post-commit echo, hook stderr, Daedalus notification); MUST NOT ship invisible automation — silent automations cannot be debugged or trusted.
Commit-time automation MUST be authored as a **native git hook** under `.githooks/` (`core.hooksPath=.githooks`) — `pre-commit`, `commit-msg`, or `post-commit` — as a thin entrypoint delegating to the body script under `.claude/hooks/bodies/git-{precommit,commit-msg,postcommit}.sh`.
When splitting work between skill and agent, MUST place in a skill if the work needs session history (orchestration, editing based on session state, user-facing slash commands, commit/push flows); MUST place in an agent if the work benefits from fresh context (adversarial review, cold-reader analysis, isolated investigation, parallel offload to protect the main context window).
Meta · Foundations
2Every fact (rule content, step definition, peer declaration, wire format, schema) MUST live in exactly one file; MUST NOT be mirrored as prose across multiple files.
When rules conflict or interpretation is ambiguous, MUST defer to Ashkaan; MUST flag conflicts for resolution; MUST NOT rewrite rules unilaterally.
Meta · Hooks
5Hooks MUST fall into one of nine allowed categories: (1) syntactic checks (lint/fmt/shellcheck/gitleaks/check-refs), (2) peer-file co-commit validation, (3) progress-doc co-commit validation, (4) checklist completion gating, (5) journal frontmatter validation, (6) memory-write redirect, (7) tool-sandbox blocks (block-context-builds, block-send-gmail), (8) session-discipline gate (refuse a tool/skill invocation that violates a session-shape invariant — e.g.
Every blocking hook MUST emit an error message naming the exact file, the exact line (when applicable), and a concrete remediation; MUST NOT say "fix the issue" without pointing at the issue.
Hooks MUST NOT subject `.md` files to syntactic checks (fmt, lint, linewidth); markdown is not code.
Every hook shell script MUST start with `set -euo pipefail` (after shebang); MUST NOT let errors silently propagate — hooks with broken state fail hard, not quietly.
Every hook shell script MUST pass `shellcheck` on commit; MUST NOT disable warnings without an inline `# shellcheck disable=SCXXXX` naming the specific issue.
Meta · Progress Docs
5`apps/README.md` and `integrations/README.md` regenerate via `.claude/hooks/bodies/git-precommit.sh` Phase A; MUST NOT manually edit either — edit the underlying per-app/per-integration README, and the index regenerates.
Journal entries MUST carry session frontmatter listing sessions worked on; MUST NOT commit journal entries with only `date` and `tags` when the session did project work — structured fields feed the telemetry aggregator.
When a commit touches files under `projects/<domain>/<date>_<slug>/` (other than that project's README), the commit MUST also update the project's README (`status`, `next`, `blocked-on`, or progress section) OR include `[progress-docs-current]` in the commit message AND state in that same message why the README is already current; MUST NOT ship project work without synchronized progress docs.
Every project README MUST have frontmatter with: `project`, `status` (active|blocked|monitor|completed), `priority` (high|medium|low) for active/blocked/monitor, `created` (YYYY-MM-DD), `tags`, `description`; when `status: blocked`, MUST use `blocked-on:` instead of `next:`; when `status: monitor`, MUST use `monitoring-until: YYYY-MM-DD` naming the observation window end; autonomous `completed` / `monitor` flips are permitted ONLY at `/close` step-2.1 under `@rule:projects-completion` (which owns the criteria) and MUST be stated in the close summary.
When `/triage` fires, the session block in today's journal MUST contain exactly one line in this format: `**Root-Cause Status:** fixed | deferred-with-project | unknown-pending-verification — <project-path or verification-step>`; `/close` reads this via grep; MUST NOT use backticks on the status label or omit the separator.
Meta · Rule Authoring
10Every new rule, hook, or enforcement MUST cite a real prior failure (journal entry, incident, PR comment) in its evidence date `[YYYY-MM-DD]`; MUST NOT ship rules citing "TBD" or hypothetical scenarios — speculative rules inflate the ruleset without reducing real failures.
A rule MAY leave the always-loaded surface ONLY via a deterministic trigger (hook / `paths:` glob / high-signal keyword where the keyword IS the rule's activation condition).
Files with peer relationships MUST declare them in frontmatter `peers: [<paths>]`; `find-peers.sh` reads frontmatter across changed files; MUST NOT hardcode peer pairs in the find-peers script — adding a peer is a 1-line frontmatter edit.
When promoting a prose rule to a mechanism (hook, linter) and retiring the prose per `@rule:one-behavior-one-surface`, MUST first verify the mechanism's coverage equals the prose's full scope; MUST NOT retire prose broader than the mechanism enforces.
When a rule body contains a literal string that looks like a URL, version, vault name, path, or namespace, MUST classify the string as either a *governance convention* (the string IS what the rule enforces — keep inline) or a *vendor detail* (the string describes an external fact that happens to appear but isn't the rule's enforcement target — reference `integrations/{name}/README.md` or add a forcing function).
A new rule MUST default to a deterministic trigger — mechanized (hook/linter) or path-scoped (`paths:` glob) — so it loads or enforces without Claude choosing.
Every rule MUST end with `[YYYY-MM-DD]` citing a journal entry or commit where the originating failure was observed; MUST NOT use "TBD", "none", or empty brackets — the evidence date is the anti-speculation anchor.
Every `.claude/rules/*.md` file MUST have frontmatter with either `paths: [<globs>]` (path-scoped) or nothing (always-loaded); MUST NOT add arbitrary frontmatter fields — Claude Code only recognizes `paths:`.
A new or edited rule body MUST be minimal-context: the imperative (`MUST`/`MUST NOT`), a `[YYYY-MM-DD]` evidence date, and at most a ≤1-line why.
Rule IDs (section slugs) MUST be stable once shipped — renaming breaks cross-file `@rule:<id>` references; MUST NOT rename a rule without updating all `enforces:` and `@rule:` citations in the same commit (linter enforces).
Meta · Skill Authoring
6The skill's markdown body MUST match the step graph in frontmatter (every step-ID mentioned in body matches an id in `steps:`, and every frontmatter step has a body section describing it); MUST NOT let body and frontmatter drift — linter checks parity.
Every rule ID in a skill's `enforces: [@rule:<id>]` field MUST resolve to an existing rule section in `.claude/rules/`; MUST NOT reference rules that don't exist — linter rejects dangling enforces.
Every step of `kind: gate` MUST specify the `tool` (`AskUserQuestion`, `shell`, `halt`, `validate-schema`, `agent`, `skill`) and `on_fail` (`halt | warn | continue`); MUST NOT use "prompt the author" as a tool — the tool name must be a concrete mechanism.
Every inter-skill handoff MUST appear in both producer's `handoffs_to:` and consumer's `handoffs_from:`; wire format declared in producer's `writes:` field MUST match consumer's `reads:` field; MUST NOT leave handoffs one-sided — linter flags asymmetry.
Every skill MUST declare in frontmatter: `name`, `description`, `argument-hint` (when the skill takes arguments), `disable-model-invocation` (explicit true/false), `enforces:` (`@rule:<id>` list — may be empty `[]` if the skill enforces no specific rules); MUST declare `peers:` when the skill loads co-located files; MUST declare `steps:` and `handoffs_to:` / `handoffs_from:` when the skill has a gated step graph.
When a skill has gates (AskUserQuestion, halt, shell checks, or inter-skill handoffs), its `steps:` frontmatter MUST declare each step's `id`, `kind` (action | gate), and either an `action:` description (for kind=action) or a `gate:` spec (for kind=gate); MUST NOT use ambiguous intermediate forms.
Meta · Telemetry
5A rule unfired for 28 days AND carrying no `enforces:` reference from a skill or hook IS a deletion candidate — that test still binds, and "fired" keeps its meaning (a mechanism ran), which no other signal may redefine.
`Workbench's apps/ai/daily-optimizer/src/lib/aggregator.ts` MUST be invoked on every optimizer run to read the last 7 days of journal frontmatter and produce the rule-firing histogram, silent-path-scope failures, root-cause-status distribution, stale-rule list, and correction-vs-rule-deltas signal.
Session telemetry emits via three surfaces: (a) mechanism-firing log via `.claude/hooks/checks/log-mechanism-fired.sh` sourced from each check script — the primary signal for whether ENFORCEMENT ran, (b) journal frontmatter `sessions:` block with `rules_should_have_fired`, `implement_audit_rounds`, `root_cause_status` — the supplementary self-noticed signal, (c) rule-loading log at `~/.local/share/claude-rules-loaded.log` via `.claude/hooks/log-instructions-loaded.sh` on the `InstructionsLoaded` event — whether a `CLAUDE.md` / `.claude/rules/*.md` file entered CONTEXT at all.
When the aggregator surfaces a standing signal about a rule, that signal MUST drive the next rule change (new rule, revised mechanism, or promotion to enforcement).
The ai-workflow-telemetry report MUST emit either formatted text or JSON each run; MUST NOT persist a separate report artifact.
Node Modules Location
1For ANY TS app in this repo that uses npm dependencies, `node_modules/` MUST live outside `apps/{name}/` and `integrations/{name}/`.
Node Stack
2**Node 22 is a trigger.dev constraint, not a repo-wide one.** It binds `apps/` code that deploys to trigger.dev and nothing else: the platform's task-runtime allowlist has no 24/26 option, and `runtime: "node-22"` resolves to `node:22.16.0-bookworm-slim` (per `@rule:trigger-dev-config-runtime`).
Pick the invocation from the runtime that will EXECUTE the script, not from a default — type-stripping is spelled differently on the two runtimes this repo uses, and each spelling is a hard error on the other.
People
3A PERSON's canonical record is their row in the dashboard's `people` table plus their `people_notes`; MUST NOT keep a second copy of those fields anywhere.
When a person is mentioned or involved in project work, MUST enrich them in the same session — their row + notes for facts, their `knowledge/people/<name>/` files (README, bio.md, mind.md, professional.md as relevant) for prose; MUST NOT defer enrichment to "later." [2026-03-12]
A person's FIELDS — name, relationship, added, birthday, location, met through — and their tagged notes are ROWS in the dashboard's `people` / `people_notes` tables, written by `/people` through the dashboard repo's `scripts/entities.ts` (documented at `workbench's apps/ai/dashboard/`); MUST NOT write them as `- **Field:**` bullets or a `## Notes` section in a README, where nothing reads them.
Portal Apps
2When making a user-facing portal change, MUST update `src/changelog.json` in the same commit; MUST NOT ship user-visible changes without a changelog entry.
MUST NOT run `npm run build`, `npm install`, or `wrangler pages deploy` locally on portal sites; portal deploys happen automatically on git push (Cloudflare Pages watches the repo).
Shell
1Every shell script MUST start with `set -euo pipefail` (after the shebang); MUST NOT rely on default shell safety — errors must surface, not silently propagate.
Style App Development
16Every SSR app MUST ship BOTH `src/pages/404.astro` (with `export const prerender = true;` so CF Pages serves the static `404.html` for direct hits) AND `src/pages/[...slug].astro` (SSR catch-all returning **200**, not 404, so Astro's ClientRouter doesn't full-page-reload during View Transitions); MUST NOT ship only one — the prerendered file misses client-side nav, the catch-all alone breaks SEO/bots.
When doing date arithmetic on calendar dates, MUST use `addMonths(dateStr, n)` and `todayLocal()` from `@theme/lib/dates`; MUST NOT use `Date` constructor + `setMonth` patterns — TZ-shift bugs are silent.
When parsing a calendar date for display, MUST use `parseCalendarDate(dateStr)` from `@theme/lib/dates`; MUST NOT use `new Date("YYYY-MM-DD")` — JS spec parses date-only strings as UTC midnight, shifting the day back for US-timezone users.
A calendar date (no time component — complaint date, close date, review date, due date) MUST be stored as `"YYYY-MM-DD"` string (the format `<input type="date">` produces); MUST NOT round-trip through `new Date()` for storage.
Theme-affecting interactive components (`ThemeSwitcher`, `Changelog`, `FeatureRequest`) MUST hydrate with `client:load`; MUST NOT use `client:idle` or `client:visible` — they need to be live before user interaction.
When creating a new internal app, MUST scaffold via `/portal-init [new-name] --from [source-portal-slug]` (clones the closest-matching live portal — `finance.wesolve.tech`, `operations.wesolve.tech`, `sales.wesolve.tech`, `dashboard.ashkaan.me` — into `~/code/[new-name]`, strips `node_modules` + `.git`, resets `name` in `package.json` + `wrangler.toml`, runs `git init`); MUST NOT hand-roll Astro/wrangler/D1 setup or recreate it from documentation.
Every app API route MUST guard with `getEmail(request)` from `@theme/lib/auth` (checks `CF-Access-Authenticated-User-Email` header first, falls back to `CF_Authorization` JWT cookie) and return `401` when null; MUST NOT roll custom auth — Cloudflare Access (Zero Trust) is the canonical auth surface.
Every internal app page MUST render inside `DashboardLayout.astro` and MUST pass its `activePage` prop so the sidebar marks the current page.
Every app MUST expose `POST /api/feature-request` (and `/api/bug-report`) as a thin re-export of `createFeatureRequestHandler` from `@theme/lib/api/feature-request`, which POSTs the submission to the `feature-request-processor` trigger.dev task and returns `{ id }`; MUST NOT skip — `FeatureRequestInline` and the sidebar `FeatureRequest` button depend on it.
When local-dev needs `locals.runtime.env` (D1 or other bindings), MUST use `wrangler pages dev -- astro dev`; plain `astro dev` runs faster but bindings are `undefined`.
MUST NOT run `npm` commands from the context repo root — run from the app folder or `~`; otherwise `package.json`/lock files/`node_modules` land in the context repo.
Every `.astro` page in an internal app MUST define `const pageInfo: PageInfoEntry` in frontmatter and pass it to `DashboardLayout` via `pageInfo={pageInfo}`; `PageInfo.tsx` reads it for the per-page context modal (purpose, mechanics, data sources, notes); presence is CLAUDE-applied (no automated check); MUST NOT ship a page without `pageInfo`.
When persisting user preferences in an app, MUST use `getItem/setItem/syncFromServer` from `@theme/lib/prefs` (synchronous localStorage read, fire-and-forget background PUT to `/api/prefs` → the portal's own `prefs` table); the app MUST expose `/api/prefs` (GET/PUT) endpoints — the edge-app scaffold includes them; MUST NOT bypass the localStorage-first pattern.
When styling or building UI in an internal app, MUST use the shared theme's canonical surface — design tokens, the typography trio, utility classes, and animation classes — from `theme/` (git subtree of `github.com/Ashkaan/internal-apps-shared.wesolve.tech`, the SSOT); MUST NOT introduce app-specific palette/font overrides, re-implement a themed pattern inline, or write custom keyframes for the standard cases.
A timestamp (precise moment in time — `createdAt`, `updatedAt`, `lastUpdated`) MUST be stored as `new Date().toISOString()` (`"2026-04-09T19:42:11.000Z"`); display via `formatTimestamp(isoStr, options?)` from `@theme/lib/dates`.
Every app `wrangler.toml` MUST declare: `name = "app-name"`, `compatibility_date = "2025-09-01"`, `compatibility_flags = ["nodejs_compat_v2"]`, `pages_build_output_dir = "dist"`, `max_duration = 30`; MUST NOT change compatibility date/flags without verifying every binding still works.
Style Biome
1The repo's TypeScript formatter + linter is Biome `^2.4.15` <!-- rule-ref: allowed --> installed at `~/.local/lib/quality/node_modules/@biomejs/biome` (per `@rule:node-modules-out-of-repo`).
Style Code Conventions
10TypeScript and shell identifiers MUST follow these casing conventions: - **Variables, functions, methods (TS):** camelCase (`dayStart`, `fetchJson()`).
Shell scripts MUST follow these formatting + safety conventions (naming covered by @rule:naming-conventions): - 2-space indentation (no tabs), 80-char hard line limit.
Exported functions MUST have an explicit return-type annotation; MUST NOT rely on inference for exports — exports are the contract.
Thrown error messages MUST be sentence case, no trailing period, active voice, with context (`throw new Error(\`Cannot refresh token for ${path}: ${r.status}\`)`); MUST NOT use vague messages ("Token refresh error.").
TypeScript imports MUST be grouped in this order, blank-line separated: (1) type imports, (2) third-party packages, (3) `integrations/` paths, (4) relative imports; MUST NOT mix groups.
When catching errors, MUST log or push to warnings; MUST NOT use empty `catch {}` — silent failures are bugs.
TypeScript code MUST fetch secrets via the canonical resolver: `getCred(CREDS.x, "field")` from `integrations/1password/op_helper.ts` (typed UUID-keyed credential map per `projects/homelab/2026-05-01_credential-architecture/`).
MUST NOT write JSDoc on internal/non-exported code — types are the documentation; MAY write a one-line `/** description */` + `@param` tags ONLY on shared utility exports (functions imported from `integrations/{name}/`).
TypeScript code MUST be explicit; MUST NOT use `Proxy`, `eval`, or runtime metaprogramming patterns — implicit behavior is hard to trace and harder to debug.
Before committing TypeScript changes, MUST pass the code-quality checks: `biome-quality` (lint), `gitleaks`, `check-refs`; MUST NOT commit with quality-gate failures — fix at the source.
Style Longform Format
2When drafting LinkedIn posts, MUST use plain text with line breaks and CAPS (sparingly); MUST NOT use markdown `**bold**`, `# headers`, or `- bullets` — LinkedIn renders them as raw characters.
When drafting blog articles or long-form posts, MUST use sequential heading levels (H1 → H2 → H3); MUST NOT skip from H1 to H3; AI does this routinely.
Trigger Dev Stack
5Every trigger.dev app under `apps/{name}/` (flat) or `apps/<domain>/{name}/` (domain member) MUST use the B2 layout: docs + config at the app root (`README.md`, `SPEC.md`, `package.json`, `tsconfig.json`, `trigger.config.ts`), task declarations under `src/tasks/*.task.ts`, pure helpers under `src/lib/*.ts`, tests under `tests/*.test.ts`.
Every `apps/{name}/trigger.config.ts` MUST set `runtime: "node-22"`; MUST NOT use `"node"` (which selects 21.7.3), `"bun"`, or any other value — one runtime across every trigger.dev bundle is the coherence guarantee, and a domain bundle's members must agree.
A trigger.dev **project** is a deploy bundle (one built image → one GHCR slot → one run-history view).
All trigger.dev apps share ONE off-repo dep cache at `~/.local/lib/trigger-dev/` (bootstrapped one-time via `bash .claude/hooks/checks/trigger-dev-build.sh bootstrap`).
Every trigger.dev dependency in the repo MUST sit on ONE `@trigger.dev/*` version, major 4 — the uniformity, not any particular number, is what is load-bearing, and the version literal lives in the mechanism, never mirrored here.