agentleFS
robmclartyrobmclarty/fascicle/AGENTS.md

fascicle

AGENTS.md4 starsChanged 15 days agoApache-2.0View on GitHub

Instructions for any coding agent (Claude Code, Codex, Cursor, Windsurf, Amp) operating in this repository. If your task is to construct a new agent that consumes the published fascicle package (rather than to modify this repo), stop here and follow docs/blueprint.md — the recommended architecture for Fascicle apps: one composition layer, three adoption tiers, markdown prompts, normalized module contracts, stub-engine testing, and ast-grep rules that enforce the boundaries. docs/leaf-arm-spine.md is the layering decision guide and examples/newsroom/main.ts the vocabulary tour. The…

  • Reads credentials
  • Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.

    Of the AGENTS.md files we read, the 31,439 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.

  • In this repository2 other files it gives its agents.

The file

Show its rules and text

We split this AGENTS.md into the rules it gives an agent

16 rules. About 0 are common, 6 are distinctive, and 10 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈6

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 10

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    pnpm check:all is the single source of truth for "done": If it exits 0, your work is complete.

    line 11
  2. Distinctive:

    Before declaring a task finished:

    line 17
  3. Very specific:

    If it fails, read .check/summary.json to find which check failed.

    line 20
  4. Very specific:

    Read the corresponding per-tool JSON (.check/lint.json, .check/dead.json, etc) for structured diagnostics.

    line 21
  5. Very specific:

    Functional/procedural: No classes. Use modules, closures, and plain data.

    line 53
  6. Very specific:

    Named exports: No default exports. Enforced by rules/no-default-export.yml.

    line 54
  7. Very specific:

    File extensions: import with .js even from .ts files (NodeNext resolution).

    line 57
  8. Distinctive:

    Cross-module access is sealed two ways: Every module is reachable only through its barrel:

    line 112
  9. Very specific:

    Adding a module: create src/<module>/index.ts, add #<module> to the root package.json imports, tsconfig.json paths, and vitest.config.ts …

    line 119
  10. Distinctive:

    Do not disable lint rules to pass the check.

    line 124
  11. Distinctive:

    Do not add dependencies casually.

    line 125
  12. Distinctive:

    Do not add a file that is not imported by something.

    line 126
  13. Very specific:

    Do not skip writing tests for new behavior.

    line 127
  14. Very specific:

    Do not bypass pnpm check:all by running individual tools and claiming done.

    line 128
  15. Distinctive:

    Do not re-baseline to clear a failure you introduced.

    line 129
  16. Very specific:

    Use these during implementation for real-time dead-code, boundary, and structural feedback, rather than waiting for the final pnpm check.

    line 138

≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
AGENTS.md

AGENTS.md

Instructions for any coding agent (Claude Code, Codex, Cursor, Windsurf, Amp) operating in this repository.

Building an app on Fascicle?

If your task is to construct a new agent that consumes the published fascicle package (rather than to modify this repo), stop here and follow docs/blueprint.md — the recommended architecture for Fascicle apps: one composition layer, three adoption tiers, markdown prompts, normalized module contracts, stub-engine testing, and ast-grep rules that enforce the boundaries. docs/leaf-arm-spine.md is the layering decision guide and examples/newsroom/main.ts the vocabulary tour. The canonical worked example is examples/pr-improve/ (rationale in its docs/architecture.md); copy its rules/ directory to enforce the blueprint in your own app. The rest of this file is about developing Fascicle itself.

The contract

pnpm check:all is the single source of truth for "done". If it exits 0, your work is complete. If it exits non-zero, it is not. No other signal counts.

The suite is checkride, configured in checkride.config.json. Each check occupies a named slot. A handful of slots run this repo's own commands (deps, exemplars, examples, prose-health, types-app, types-e2e, e2e, learn), and everything else is a checkride built-in.

pnpm check runs the default set (every slot except the opt-in ones) and is what you should use in tight feedback loops. pnpm check:all adds the opt-in slots (Stryker mutation, the build → publint/attw/pack/smoke/snippets packaging gate, the learn walk through the course, and the Playwright e2e slot) and is the gate before declaring done. The one slot neither of them runs is security, the dependency audit. It runs only as pnpm check:security, and the note on its entry in checkride.config.json says why.

Before declaring a task finished:

  1. Run pnpm check:all.
  2. If it fails, read .check/summary.json to find which check failed.
  3. Read the corresponding per-tool JSON (.check/lint.json, .check/dead.json, etc) for structured diagnostics.
  4. Fix the root cause, not the symptom.
  5. Re-run pnpm check:all.

The same reports exist for a red CI run. checkride prints one pass/fail line per slot and writes the tool's actual output to .check/, so the workflow log alone will tell you that types failed without telling you why. Every workflow that runs the suite uploads .check/ as an artifact when it fails, so steps 2 and 3 work the same way against a remote run: download the artifact from the run page, open summary.json, then read the per-tool report it names. The coverage/ and mutation/ HTML trees are left out because they are the bulk of the directory and nobody reads them during triage.

The baseline

checkride.baseline.json grandfathers the diagnostics that predate the gate, so a slot reporting N baselined (grandfathered) is green. Only findings absent from the baseline fail a run, which is what makes the gate ratchet: existing debt is frozen, new debt is blocked.

Never run checkride baseline to make a failure go away. Re-baselining is a deliberate act (recording paid-down debt, or absorbing a tool's fingerprint change) and belongs in its own commit with the reason stated. A finding your change introduced gets fixed, not grandfathered.

Tight feedback loops

During iteration, use narrower commands for faster turnaround:

pnpm check                     # default set (excludes the opt-in slots)
pnpm check --bail              # stop at first failure
pnpm check --only types,lint   # just the fast checks
pnpm check --changed           # affected-only, against the git diff
pnpm check --include mutation  # default set plus opt-in mutation
pnpm check:security            # the dependency audit, which no other run includes
pnpm test:watch                # watch-mode tests while implementing
pnpm exec tsc --noEmit         # just types

pnpm check:all (every slot but security, the opt-in ones included) is the final gate before declaring done. Stryker's mutation slot is the slowest and is opt-in for exactly this reason; incremental mode keeps re-runs cheap once the shared baseline at stryker.incremental.json is up to date. build re-bundles, then publint, attw, pack, smoke, and snippets validate the npm packaging against dist/ (manifest, type resolution, import liveness, doc snippets vs the built .d.ts), so surface changes fail here instead of at release time. learn walks the course in docs/learn/ against the same dist/: it writes each lesson's files into a scratch project, runs the lesson's commands, and holds what they print to the lesson's text, so a change that breaks a lesson fails here too. When you add or remove a code block in a lesson, update the block index that scripts/check-learn.mjs reads it by.

Conventions

  • TypeScript strict mode. Everything. No any, no ! non-null assertions without justification.
  • Functional/procedural. No classes. Use modules, closures, and plain data. Enforced by rules/no-class.yml.
  • Named exports. No default exports. Enforced by rules/no-default-export.yml.
  • Naming: snake_case for variables and functions, PascalCase for types and interfaces, SCREAMING_SNAKE_CASE for constants. Exports are enforced by rules/snake-case-exports.yml. A factory's trailing argument is named for whether the caller can drop it. It is XConfig when the argument is required (retry(inner, config), loop(config), compose(inner, config)), and XOptions when it can be omitted (sequence(children, options?), pipe(inner, fn, options?)). The two tests coincide in src/core and src/composites, because an argument that carries a required field is one you cannot leave out. The display label is spelled name in either case, and it always means display, because identity is the id.
  • Function docs. Every module-level function carries a /** ... */ block directly above it: one line saying what it is, then optional paragraphs on why it behaves the way it does. Comments inside function bodies use //. Comments state what the code does now, in the present tense, with no design-history references. Enforced by rules/function-comment-required.yml.
  • File extensions: import with .js even from .ts files (NodeNext resolution).
  • Prose style. Markdown and TypeScript doc comments are linted by vale as the prose slot; the config is .vale.ini and the rules are plain YAML in .vale/styles/Repo/. e.g., and i.e., are house style and want both periods and the comma. No sentence opening on "there is"/"there are" (name the subject that acts instead). No hyphen after an -ly adverb. An aside takes round brackets, never a pair of em dashes (a single em dash between clauses is a pause, and pauses are fine). One semicolon to a sentence, and one colon. None of the generated-prose fingerprints in Drift.yml. The project name is capitalized in prose (Fascicle) and lowercase only as an identifier (the fascicle package, the fascicle/* subpaths, the fascicle-viewer binary). The rules belong to this repo: disagree with one and edit or delete it rather than suppressing it line by line. The rules police mechanics; the exemplars carry the voice.
  • Runtime messages are prose too. Vale maps ts = js, which lints doc comments and leaves the code alone, so it never sees a string literal. The two mechanical rules a user-facing message needs as much as a paragraph does are mirrored as ast-grep in rules/no-latin-abbrev-in-strings.yml and rules/no-em-dash-pair-in-strings.yml, and they run in the struct slot. The rest is judgment. A message says what happened, then says what to do, and it carries the value the reader has to act on. The shape to copy is no provider specified: pass `provider` to generate() or set `defaults.provider` (configured: anthropic, ollama).
  • Tests colocated in __tests__/: unit tests for foo.ts live at __tests__/foo.test.ts next to it (for example, src/core/branch.ts ↔ src/core/__tests__/branch.test.ts). Cross-cutting tests (integration, signal handling, fixtures) live under src/<module>/test/ instead, which is outside the source-semantics rules and spell-check.
  • Coverage floor: 70% lines/functions/branches/statements. Raise it as the codebase matures.
  • Architectural boundaries are enforced. The rest of rules/ uses ast-grep to police layer separation between core, engine, adapters, and the claude_cli provider — for example, no adapter imports in core, no cross-composer imports, no process.env reads outside the audited exceptions, no provider SDKs outside src/engine/providers/, no child_process in the engine outside claude_cli or in acp outside run_command. fallow.toml adds a directory-level default-deny boundary DAG on top. Read rules/ and the fallow.toml boundaries before adding a cross-module import.

Prose voice

.vale/exemplars/ holds the voice: how a sentence should sound. Every file is the author's own published writing, verbatim, and none of it is later than 2022. Read all of them before writing or editing prose (markdown, doc comments); together they run about 4.2k tokens. When your draft and an exemplar disagree about how a sentence should sound, the exemplar wins.

They are hand-written and human-owned. Never edit, rewrite, add to, or generate one. The exemplars check (scripts/check-exemplars.mjs) fails on any difference against HEAD, whitespace included, so a stray formatting pass trips it the same way a rewrite would. A human adding one deliberately runs EXEMPLARS_UNLOCKED=1 pnpm check.

The directory is not linted and does not ship to npm. Vale checks mechanics, and a sentence in the author's own voice is not something a mechanical rule should overrule.

For form rather than voice, read what this repo already does. A doc comment follows the ones already under src/, a release note follows CHANGELOG.md, a troubleshooting entry follows docs/troubleshooting.md, and a design proposal follows examples/pr-improve/SPEC.md.

Prose shape

Vale checks one sentence and the exemplars carry the voice. Neither can see the way a whole file goes wrong, which is prose that passes every rule while every clause gets packed into a modifier until the reader has to reconstruct the verb. The prose-health check measures that directly, over docs/ and the top-level markdown, and node scripts/prose-health.mjs --report prints the table.

It ratchets each file against .prose-health.json instead of against a target, which is what lets a table of exports score a tenth of what an essay scores and stay green forever, as long as it doesn't get worse. Re-baselining is a deliberate act with the same weight as checkride baseline, so it belongs in its own commit with the reason stated.

The number is not the goal, and you raise it honestly by rewriting a compressed sentence into a finite clause. Inserting the word the metric counts also raises it, and makes the prose worse. The method, the traps that break markdown structure, and the reason there's no regex for the constructions this can't count are in .claude/skills/prose/SKILL.md.

Source layout

This is a single package. All source lives under src/, organized as deep modules: src/<module>/ (core, engine, composites, agents, adapters, mcp, acp, stdio, ui, otel, policy, schema, testing, viewer), each with a barrel index.ts that is its only public face. The umbrella surface sits at the src/ root (index.ts, model_call.ts, forward_standard_env.ts); that is what bundles to npm as fascicle. src/diagram_cli.ts and src/diagram_bin.ts sit beside them but aren't part of that surface. They're the fascicle-diagram bin (its decisions, then its process glue), and tsdown bundles diagram_bin.ts as its own entry, which only the bin shim from scripts/build.mjs imports. One doc ships as well: the build copies docs/learn/agent-primer.md into the package as dist/AGENTS.md, with its links pointed at the release's tag on GitHub, so an edit to the primer changes what every install reads. The acp, adapters, agents, mcp, otel, stdio, testing, ui, and viewer modules are additionally published as fascicle/<module> subpaths. The 7 apps under examples/*/ are the only other workspace members; they depend on the library via fascicle: workspace:*.

Barrels are import/export only. An index.ts contains only import, export … from, export { … }, and export type statements (bare side-effect imports are fine). No runtime logic: module logic lives in a named sibling file (for example, create_engine in create_engine.ts, start_viewer in start_viewer.ts) that the barrel re-exports. Enforced by rules/no-logic-in-barrel.yml.

Cross-module access is sealed two ways. Every module is reachable only through its barrel:

  • Use the #<module> import alias for cross-module imports, never a relative path that escapes the module dir. Use import { x } from '#core', never import { x } from '../core/x.js'. The #-aliases are declared in the root package.json imports (mapping each to src/<module>/index.ts), mirrored in tsconfig.json paths and vitest.config.ts resolve.alias. otel has no alias yet because no sibling module imports it. Because only barrels are mapped, deep imports through the alias are impossible; rules/no-cross-module-relative-import.yml closes the relative-path channel. (Cross-cutting harnesses under src/<module>/test/ are exempt — they are spawned as child node processes that cannot resolve #-aliases, so they reach siblings relatively.)
  • engine imports core type-only (import type), the one value exception being src/engine/errors.ts re-exporting aborted_error.
  • Examples import the published surface (fascicle and its fascicle/* subpaths), so they are copy-pasteable by npm consumers. Resolved via root tsconfig.json paths + vitest.config.ts resolve.alias.
  • Dependencies live in the one root package.json. Runtime deps are direct; ai, zod, and every provider SDK are optional peers. The "core depends on no npm packages" / "engine (outside providers/) depends on no npm packages" invariants are enforced at the import level by rules/no-core-npm-dep.yml and rules/no-engine-npm-dep.yml (the single manifest can no longer express per-module dependency shape).
  • Boundaries are enforced by rules/*.yml + fallow.toml. The fallow [[boundaries.rules]] DAG is directory-level default-deny (each module lists the siblings it may import); the umbrella (src/*.ts) is unconstrained. Mirrors the ast-grep rules.
  • Adding a module: create src/<module>/index.ts, add #<module> to the root package.json imports, tsconfig.json paths, and vitest.config.ts resolve.alias, and add a [[boundaries.rules]] entry to fallow.toml. pnpm check must still exit 0.
  • One set of root configs. tsconfig.json, vitest.config.ts, fallow.toml, cspell.json, stryker.config.mjs, and rules/ glob across src/**.

What NOT to do

  • Do not disable lint rules to pass the check. If a rule is wrong for a case, discuss first or use a scoped inline suppression with a comment explaining why.
  • Do not add dependencies casually. Every new dep is surface area. Fallow will catch unused ones.
  • Do not add a file that is not imported by something. Fallow will flag it.
  • Do not skip writing tests for new behavior. Stryker runs as the opt-in mutation slot (pnpm check:all) and will catch tests that pass trivially.
  • Do not bypass pnpm check:all by running individual tools and claiming done.
  • Do not re-baseline to clear a failure you introduced. See "The baseline" above.

MCP tools available

Both are wired in .mcp.json:

  • fallow — structured codebase analysis. Call analyze, check_changed, find_dupes, check_health, fix_preview, fix_apply, or project_info.
  • ast-grep — structural code search and rule authoring. Useful when adding or debugging rules in rules/ or finding AST-level patterns the plain grep tools can't express.

Use these during implementation for real-time dead-code, boundary, and structural feedback, rather than waiting for the final pnpm check.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.