Summary
TC's per-project CLAUDE.md is regenerated from the DB-stored global rules on every session launch. PR-driven raw-file edits to CLAUDE.md are silently discarded the next time TC restarts or syncs a project, with no warning to the operator. The rule additions live on in git log but are functionally absent from the live ruleset.
This trap bit the 2026-05-23 session on TC-v3 itself: PRs #234 (versioning bump-level table) and #236 (stale-plan archive rule) merged cleanly, their CLAUDE.md deltas landed in main, and both were stripped on the next TC restart. The "structural lessons codified in CLAUDE.md" claim we celebrated in those PR descriptions was technically true in git history and functionally false in the running ruleset.
Root cause
lib/engines.js#_generateClaudeMd (~line 341) composes CLAUDE.md from:
data/default-global-rules.md (factory default, 11 lines — only the seed for fresh installs)
- The user's customized global rules stored at
_globalRulesPath() (loaded via store.globalRules.load())
- Per-project + methodology data
The regeneration runs on:
- Every session launch (
lib/sessions.js#launchSession)
- Every project create/update via API (
lib/projects.js#updateProject when engine/methodology changes)
- The startup sync at
lib/projects.js#syncAllProjects()
There is no merge between the on-disk CLAUDE.md and the regenerated content. The file is overwritten in place.
How to detect if you're affected
Run this in your TC-v3 clone:
# All commits that touched CLAUDE.md
git log main --oneline -- CLAUDE.md
# For each commit that added a `**Rule:` or `##` block, check whether
# the same text appears in the live ruleset. If not, your PR was clobbered.
curl -sk https://localhost:3102/api/rules/global \
| python3 -c "import json, sys; print(json.load(sys.stdin)['content'])" \
> /tmp/tc-db-rules.md
# Spot-check known PR-added rules:
grep "Archive plans whose chunk has shipped" /tmp/tc-db-rules.md # #236 — should match after recovery
grep "TC's .version-bump. wrap step picks the bump level" /tmp/tc-db-rules.md # #234 — should match after recovery
If any rule from git log is missing from /tmp/tc-db-rules.md, it was silently clobbered.
How to recover (if you're affected)
The rules from #234 and #236 have been re-merged into the DB content via store.globalRules.save() on the TC-v3 dev clone — but other clones still need to recover on their own. Two paths:
Option 1 — recover by re-applying via the API (one-shot per clone):
# Pull the rule blocks from the PR diffs:
git show 463e57a -- CLAUDE.md # #234 bump-level rule + table
git show 8830e5c -- CLAUDE.md # #236 archive-plans rule + issue-state check
# Splice them into your current rules and PUT back:
curl -sk https://localhost:3102/api/rules/global # read current content
# … edit locally, then …
# (Note: PUT /api/rules/global has a 10KB body limit per #235-era constants;
# if your merged content exceeds it, write via a small node script that
# calls store.globalRules.save(content) directly. See the recovery PR for
# the exact pattern.)
Option 2 — recover via the landing-page editor: paste the rule blocks into the global-rules editor (gear icon → Global Rules) and save.
Either path puts the content into the DB, where it survives regeneration.
Other engine configs affected (same bug, less commonly hit)
_generate* functions in lib/engines.js cover Claude (CLAUDE.md), Codex (.codex.yaml), Aider (.aider.conf.yml), Gemini (GEMINI.md). All four are regenerated. PR-driven edits to any of them have the same fate as CLAUDE.md. In practice CLAUDE.md is the most hand-edited because TC-v3 itself uses Claude Code.
Proposed structural fix (separate work)
Multiple design options, none decided yet:
- (A) Tracked-file canonical source. Make
data/default-global-rules.md (or a renamed equivalent) the canonical source. TC's startup sync detects when the file is newer than the DB-stored content and offers to merge or auto-applies non-conflicting additions. PR-driven rule changes edit the tracked file; existing user customizations stored in the DB are preserved via a merge strategy.
- (B) Append-only marker pattern. TC's regeneration preserves content below a
<!-- @user-additions --> marker in CLAUDE.md. PR-driven edits go below the marker. Loses the architectural pretense that the file is "generated" but works for incremental additions.
- (C) Pre-commit / pre-merge guard. A git hook in TC's own repo (or a CI check) blocks commits that touch
CLAUDE.md and tells the contributor to use the API or the landing-page editor. Prevents the trap but doesn't fix the architecture.
- (D) Detect-and-warn on regeneration. On overwrite, diff the existing
CLAUDE.md against the regenerated content. If they differ in non-trivial ways (operator-added rules vs whitespace), log a loud warning AND surface in a banner. Operator decides how to reconcile.
Recommendation pending. I'll open this as a discussion thread; the design call belongs to whoever picks up the structural work.
Refs
Summary
TC's per-project
CLAUDE.mdis regenerated from the DB-stored global rules on every session launch. PR-driven raw-file edits toCLAUDE.mdare silently discarded the next time TC restarts or syncs a project, with no warning to the operator. The rule additions live on ingit logbut are functionally absent from the live ruleset.This trap bit the 2026-05-23 session on TC-v3 itself: PRs #234 (versioning bump-level table) and #236 (stale-plan archive rule) merged cleanly, their
CLAUDE.mddeltas landed inmain, and both were stripped on the next TC restart. The "structural lessons codified in CLAUDE.md" claim we celebrated in those PR descriptions was technically true in git history and functionally false in the running ruleset.Root cause
lib/engines.js#_generateClaudeMd(~line 341) composesCLAUDE.mdfrom:data/default-global-rules.md(factory default, 11 lines — only the seed for fresh installs)_globalRulesPath()(loaded viastore.globalRules.load())The regeneration runs on:
lib/sessions.js#launchSession)lib/projects.js#updateProjectwhen engine/methodology changes)lib/projects.js#syncAllProjects()There is no merge between the on-disk
CLAUDE.mdand the regenerated content. The file is overwritten in place.How to detect if you're affected
Run this in your TC-v3 clone:
If any rule from
git logis missing from/tmp/tc-db-rules.md, it was silently clobbered.How to recover (if you're affected)
The rules from #234 and #236 have been re-merged into the DB content via
store.globalRules.save()on the TC-v3 dev clone — but other clones still need to recover on their own. Two paths:Option 1 — recover by re-applying via the API (one-shot per clone):
Option 2 — recover via the landing-page editor: paste the rule blocks into the global-rules editor (gear icon → Global Rules) and save.
Either path puts the content into the DB, where it survives regeneration.
Other engine configs affected (same bug, less commonly hit)
_generate*functions inlib/engines.jscover Claude (CLAUDE.md), Codex (.codex.yaml), Aider (.aider.conf.yml), Gemini (GEMINI.md). All four are regenerated. PR-driven edits to any of them have the same fate as CLAUDE.md. In practice CLAUDE.md is the most hand-edited because TC-v3 itself uses Claude Code.Proposed structural fix (separate work)
Multiple design options, none decided yet:
data/default-global-rules.md(or a renamed equivalent) the canonical source. TC's startup sync detects when the file is newer than the DB-stored content and offers to merge or auto-applies non-conflicting additions. PR-driven rule changes edit the tracked file; existing user customizations stored in the DB are preserved via a merge strategy.<!-- @user-additions -->marker inCLAUDE.md. PR-driven edits go below the marker. Loses the architectural pretense that the file is "generated" but works for incremental additions.CLAUDE.mdand tells the contributor to use the API or the landing-page editor. Prevents the trap but doesn't fix the architecture.CLAUDE.mdagainst the regenerated content. If they differ in non-trivial ways (operator-added rules vs whitespace), log a loud warning AND surface in a banner. Operator decides how to reconcile.Recommendation pending. I'll open this as a discussion thread; the design call belongs to whoever picks up the structural work.
Refs
store.globalRules.save()call (the API has a 10KB body limit that the merged content exceeded). See the recovery PR for the recovery script.