Skip to content
ƒtsforgev0.52.0
19

Where do I change X?

5 min read

The harness is ~97k lines across 30 subsystems. Most changes touch two or three files and a registration point, and the registration point is the part that is hard to find. This page is the index.

Paths are relative to packages/core/src/. Line numbers move; the symbol names are the durable part, so grep for those if a number has drifted.

The one people get wrong, because registration is spread over four files.

StepWhere
1. Name itTOOL_NAME in agent/agent.constants.ts, also where the JSON schema the model sees lives
2. Write the handlerloop/tools/<your-tool>.ts
3. Register itHANDLERS in loop/tools/execute-tool.ts
4. Advertise ittoolsFor() in loop/turn.ts

Step 3 is typed Record<ToolName, ToolHandler>, so the compiler will not let you skip it. Add the name in step 1 and typecheck tells you exactly what is missing.

If the tool mutates the workspace it must surface mutated (and edit/create for file writes) on its event, or the gate will not re-run and a stale green can be reported as done.

Write an async function yourMode(args: ICliArgs): Promise<number> in cli.ts, then add the dispatch branch in main(). The Promise<number> is the contract. It is the process exit code, and it is how the generated entry-point list finds your command. Document it in reference/commands.mdx.

Create loop/<your-stack>/ and implement the five seams described in Adapters and seams. Register it at a composition root, cli.ts or cli/**, never from inside the core loop, which an ESLint rule forbids from importing an adapter at all. loop/boringstack/ is the worked example.

mcp/registry.ts holds the client. Servers are declared in the user’s tsforge.config.json, parsed by config/tsforge-config.ts.

I want toGo to
Change when the loop calls a run finishedsettleGate / evaluateGate in loop/turn.ts
Change how the gate is composedgate/gate-runner.ts (the IGate contract), gate/core-gate.ts
Change the stage order for BoringStackloop/boringstack/gate-stages.ts
Change how tool output becomes errorsvalidate/parse.ts and the parsers beside it
Change what the write-time linter enforcesgate/linter.ts

Never relax a rule to get past a failure. The gate is the only signal that the work is real. Fix the thing that makes the model satisfy it.

I want toGo toThen
Add a rule to an existing packrule-packs/<pack>/rules/<rule>.tsexport it from the pack’s index.ts
Create a whole packrule-packs/<pack>/index.tsadd to RULE_PACKS in rule-packs/index.ts, then a descriptor in PACK_REGISTRY (stack-detection/packs.ts)
Add a non-AST rulemeta-rules/rules/<category>/register in META_RULES (meta-rules/registry.ts); add to PER_WRITE_META_RULES only if it must run on every write

After any of these run bun run rules:docs && bun run rules:build. CI fails if the generated catalog drifts from the source.

I want toGo to
Change the system promptloop/prompt/prompt.ts; the baseline role text is constitution/baseline.ts
Change per-call decisions (thinking budget, tool filtering)loop/model-call.ts
Change how the loop steers a stuck runbuildSteerMessage in loop/feedback/steer.ts; the rungs are EscalationRung in loop/loop.types.ts
Change rule help shown on a gate failureloop/feedback/rule-docs.ts and its siblings
Support a new model providerinference/openai-compatible.ts; per-model config is IModelEntry in models-config.ts
Change reasoning handling for a model familyinference/reasoning-profile.ts
Change what counts as degenerate outputinference/stream-guard.ts
I want toGo to
Add a CLI flagICliArgs in cli/args.ts, where parsing and recipe overlay also live, pure and unit-tested in tests/cli.test.ts
Add an environment flagconfig/flags.ts
Change tsforge.config.jsonconfig/tsforge-config.ts
Change a strictness profileconfig/profiles.ts
Change what a session storessession-store.ts; the structured event log is loop/ledger-writer.ts

bun run validate is the merge bar: typecheck, lint, format, tests, PTY end-to-end. It is the same gate the harness holds your code to.

Two house rules that catch most review comments:

  • A test is not coverage until it has failed. Break the line and watch the test go red. An assertion that cannot fail proves nothing about the code beneath it.
  • Scripts orchestrate; src/ decides. Anything it would be a bug to get wrong belongs in src/, where a test can import it. The generated map lists any import that violates this under Imports that leave src/.