nobrainer-tech-flow 2.4.1 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/README.md +14 -8
  5. package/docs/INSTALL.md +36 -22
  6. package/docs/MIGRATION_TO_FLOW.md +14 -0
  7. package/docs/SKILL_CURATION.md +8 -2
  8. package/docs/releases/v2.5.0.md +190 -0
  9. package/docs/releases/v2.6.0.md +73 -0
  10. package/gemini-extension.json +1 -1
  11. package/package.json +1 -1
  12. package/plugin.json +1 -1
  13. package/scripts/install.py +16 -7
  14. package/scripts/install.sh +129 -17
  15. package/scripts/install_personalization.py +51 -16
  16. package/scripts/install_skills.py +34 -5
  17. package/scripts/recommend_flow_setup.py +102 -12
  18. package/scripts/validate_skills.py +2 -1
  19. package/skills/nobrainer-build/SKILL.md +6 -2
  20. package/skills/nobrainer-context/SKILL.md +113 -0
  21. package/skills/nobrainer-context/references/clients.md +115 -0
  22. package/skills/nobrainer-context/references/consolidate.md +66 -0
  23. package/skills/nobrainer-context/scripts/context_inventory.py +425 -0
  24. package/skills/nobrainer-skill-doctor/SKILL.md +4 -4
  25. package/skills/nobrainer-tech-flow/SKILL.md +3 -3
  26. package/skills/nobrainer-tech-flow/VERSION +1 -1
  27. package/skills/nobrainer-tech-flow/references/daily-update.md +5 -2
  28. package/skills/nobrainer-tech-flow/references/delivery.md +17 -2
  29. package/skills/nobrainer-tech-flow/references/optional-decisions.md +5 -3
  30. package/skills/nobrainer-tech-flow/references/routing.md +1 -1
  31. package/skills/nobrainer-tech-flow/scripts/typed_decisions.py +20 -9
  32. package/skills/nobrainer-codex-context/SKILL.md +0 -111
  33. /package/skills/{nobrainer-codex-context → nobrainer-context}/scripts/codex_context.py +0 -0
@@ -107,6 +107,85 @@ def recorded_setting(client: str, home: Path, destination: object, target: objec
107
107
  return None
108
108
 
109
109
 
110
+ def recorded_skill(name: object) -> bool:
111
+ """A name a setup record may hold: a current module, or one a release retired."""
112
+
113
+ return isinstance(name, str) and (name in BY_SKILL or name in _sibling(INSTALL_SKILLS).RETIRED_LEGACY)
114
+
115
+
116
+ def owns_link(target: Path, name: str) -> bool:
117
+ """True when target is this checkout's link for name; a retired module's link dangles."""
118
+
119
+ if name in BY_SKILL:
120
+ return links_to(target, SKILLS_DIR / name)
121
+ return _sibling(INSTALL_SKILLS).is_exact_legacy_link(target, name)
122
+
123
+
124
+ def recorded_retired_args(record: dict | None) -> list[str]:
125
+ """install_skills.py flags for retired modules a setup record says this setup linked."""
126
+
127
+ names = (record or {}).get("created_skills", [])
128
+ return [arg for name in names if isinstance(name, str) and name not in BY_SKILL for arg in ("--recorded-retired", name)]
129
+
130
+
131
+ def claim_retired_links(destination: Path, record: dict | None) -> list[tuple[Path, Path, tuple]]:
132
+ """Move the retired links a record lists aside, verifying each moved entry.
133
+
134
+ The claim is install_skills.py's atomic claim-and-verify: an entry that changed after
135
+ the check is put back untouched. The claims are deleted only once the whole setup run
136
+ has succeeded, and put back on any failure before that.
137
+ """
138
+
139
+ skills = _sibling(INSTALL_SKILLS)
140
+ claims = []
141
+ try:
142
+ for name in (record or {}).get("created_skills", []):
143
+ if not isinstance(name, str) or name in BY_SKILL:
144
+ continue
145
+ target = destination / name
146
+ if not skills.is_exact_legacy_link(target, name):
147
+ continue
148
+ try:
149
+ claim, expected = skills.claim_legacy_link(target, name)
150
+ except skills.ClaimRestoreError:
151
+ raise
152
+ except RuntimeError as exc:
153
+ print(f"PRESERVED: {target} changed during the setup and is left as it is ({exc})")
154
+ continue
155
+ claims.append((target, claim, expected))
156
+ except (OSError, skills.ClaimRestoreError) as exc:
157
+ errors = restore_retired_links(claims)
158
+ if errors:
159
+ raise skills.ClaimRestoreError(f"{exc}; {'; '.join(errors)}") from exc
160
+ raise
161
+ return claims
162
+
163
+
164
+ def restore_retired_links(claims: list[tuple[Path, Path, tuple]]) -> list[str]:
165
+ """Put claimed retired links back; return what could not be restored."""
166
+
167
+ errors = []
168
+ for target, claim, expected in reversed(claims):
169
+ try:
170
+ # Moves the entry back without replacing a new one, and removes the private folder.
171
+ _sibling(INSTALL_SKILLS).restore_claim(target, claim, expected)
172
+ except (OSError, RuntimeError) as exc:
173
+ errors.append(f"retired link restore failed: {exc}")
174
+ return errors
175
+
176
+
177
+ def commit_retired_links(claims: list[tuple[Path, Path, tuple]]) -> None:
178
+ """Delete the claimed retired links once the setup run has succeeded."""
179
+
180
+ for target, claim, expected in claims:
181
+ if _sibling(INSTALL_SKILLS).entry_fingerprint(claim) == expected:
182
+ claim.unlink()
183
+ claim.parent.rmdir()
184
+ print(f"REMOVED_RETIRED: {target}")
185
+ else:
186
+ print(f"BACKUP_PRESERVED: the claimed retired link changed and is kept: {claim}")
187
+
188
+
110
189
  def links_to(target: Path, source: Path) -> bool:
111
190
  """True when target is a symlink resolving to source; loops and dangling links are not."""
112
191
 
@@ -144,7 +223,7 @@ ITEMS = (
144
223
  Item("14", "nobrainer-spec-driven-development", "Specifications", ("software-development", "product"), "define durable contracts for architectural changes"),
145
224
  Item("15", "nobrainer-rca", "Root-cause analysis", ("software-development", "operations"), "diagnose repeated or causally unclear failures"),
146
225
  Item("16", "nobrainer-team", "Capability-to-role design", ("software-development", "operations"), "assign available capabilities to bounded work"),
147
- Item("17", "nobrainer-codex-context", "Project context", ("software-development", "operations"), "inspect and maintain project instructions and context"),
226
+ Item("17", "nobrainer-context", "Project instructions", ("software-development", "operations"), "one tidy set of project instructions for every coding agent"),
148
227
  Item("18", "nobrainer-wiki", "Durable knowledge", ("research", "product", "operations"), "query and maintain a sourced project knowledge base"),
149
228
  )
150
229
  BY_ID = {item.id: item for item in ITEMS}
@@ -604,10 +683,10 @@ def run(command: list[str], hide: tuple[str, ...] = ()) -> tuple[int, str]:
604
683
 
605
684
  def remove_exact_created_links(destination: Path, names: list[str]) -> None:
606
685
  for name in names:
607
- if name not in BY_SKILL:
686
+ if not recorded_skill(name):
608
687
  continue
609
688
  target = destination / name
610
- if links_to(target, SKILLS_DIR / name):
689
+ if owns_link(target, name):
611
690
  target.unlink()
612
691
 
613
692
 
@@ -783,7 +862,7 @@ def read_state_file(path: Path) -> dict[str, object]:
783
862
  or not isinstance(loaded.get("destination"), str)
784
863
  or not isinstance(loaded.get("created_skills"), list)
785
864
  or not isinstance(loaded.get("profile"), dict)
786
- or any(not isinstance(name, str) or name not in BY_SKILL for name in loaded.get("created_skills", []))
865
+ or not all(recorded_skill(name) for name in loaded.get("created_skills", []))
787
866
  ):
788
867
  raise ValueError(f"refusing to overwrite unrecognized setup state: {path}")
789
868
  try:
@@ -997,7 +1076,7 @@ def rollback(args: argparse.Namespace, state_file: Path) -> int:
997
1076
  return 3
998
1077
  destination = Path(state["destination"])
999
1078
  created = state.get("created_skills", [])
1000
- if not isinstance(created, list) or len(created) != len(set(created)) or any(name not in BY_SKILL for name in created):
1079
+ if not isinstance(created, list) or len(created) != len(set(created)) or not all(recorded_skill(name) for name in created):
1001
1080
  print("ERROR: rollback state contains invalid skill ownership entries", file=sys.stderr)
1002
1081
  return 3
1003
1082
  try:
@@ -1066,7 +1145,7 @@ def rollback(args: argparse.Namespace, state_file: Path) -> int:
1066
1145
  for name in created:
1067
1146
  target = destination / name
1068
1147
  if target.is_symlink():
1069
- if not links_to(target, SKILLS_DIR / name):
1148
+ if not owns_link(target, name):
1070
1149
  print(f"PRESERVED: changed target {target}")
1071
1150
  return 3
1072
1151
  elif target.exists():
@@ -1086,8 +1165,7 @@ def rollback(args: argparse.Namespace, state_file: Path) -> int:
1086
1165
  removed: list[str] = []
1087
1166
  for name in created:
1088
1167
  target = destination / name
1089
- source = SKILLS_DIR / name
1090
- if links_to(target, source):
1168
+ if owns_link(target, name):
1091
1169
  target.unlink()
1092
1170
  removed.append(name)
1093
1171
  elif target.exists() or target.is_symlink():
@@ -1384,7 +1462,7 @@ def main(argv: list[str] | None = None) -> int:
1384
1462
  # Inspect every target before writing any of them. This keeps a later
1385
1463
  # conflict from leaving a partial selection installed.
1386
1464
  for name in install_names:
1387
- command = [sys.executable, str(ROOT / "scripts" / "install_skills.py"), "--client", args.client, "--dest", str(destination), "--skill", name]
1465
+ command = [sys.executable, str(ROOT / "scripts" / "install_skills.py"), "--client", args.client, "--dest", str(destination), "--skill", name, *recorded_retired_args(previous)]
1388
1466
  code, _ = run(command, hide=("NOTE: ",))
1389
1467
  if code:
1390
1468
  return code
@@ -1394,7 +1472,7 @@ def main(argv: list[str] | None = None) -> int:
1394
1472
  profile_preimage = read_file_preimage(profile_target)
1395
1473
  if args.apply:
1396
1474
  for name in install_names:
1397
- command = [sys.executable, str(ROOT / "scripts" / "install_skills.py"), "--client", args.client, "--dest", str(destination), "--skill", name, "--apply"]
1475
+ command = [sys.executable, str(ROOT / "scripts" / "install_skills.py"), "--client", args.client, "--dest", str(destination), "--skill", name, "--apply", *recorded_retired_args(previous)]
1398
1476
  code, _ = run(command, hide=("NOTE: ",))
1399
1477
  if code:
1400
1478
  remove_exact_created_links(destination, new_skills)
@@ -1428,7 +1506,9 @@ def main(argv: list[str] | None = None) -> int:
1428
1506
  "backup": recorded_backup,
1429
1507
  "written_sha256": hashlib.sha256(profile_target.read_bytes()).hexdigest(),
1430
1508
  }
1431
- created = sorted(set(previous.get("created_skills", [])) | {name for name in new_skills if (destination / name).is_symlink()})
1509
+ # A retired module's link is gone once retired; then the record drops its name.
1510
+ kept = {name for name in previous.get("created_skills", []) if name in BY_SKILL or owns_link(destination / name, name)}
1511
+ created = sorted(kept | {name for name in new_skills if (destination / name).is_symlink()})
1432
1512
  next_state = {"destination": str(destination), "created_skills": created, "profile": profile}
1433
1513
  # Folders an earlier run made stay in the record as recorded, links already resolved
1434
1514
  # then. Resolving them again would follow a link put in their place since, and hand the
@@ -1443,10 +1523,18 @@ def main(argv: list[str] | None = None) -> int:
1443
1523
  # a run makes a folder, so that its undo follows the rule it was made under.
1444
1524
  if created_dirs or not previous or "created_dirs" in previous:
1445
1525
  next_state["created_dirs"] = created_dirs
1526
+ # The retired links the record lists go last, after every other write, and come back
1527
+ # if the record cannot be saved.
1528
+ retired_claims = []
1446
1529
  try:
1530
+ retired_claims = claim_retired_links(destination, previous)
1531
+ retired_names = {target.name for target, _, _ in retired_claims}
1532
+ next_state["created_skills"] = [name for name in created if name not in retired_names]
1447
1533
  save_state(state, next_state)
1448
- except (OSError, TypeError, ValueError) as exc:
1534
+ except (OSError, RuntimeError, TypeError, ValueError) as exc:
1449
1535
  rollback_errors: list[str] = []
1536
+ if isinstance(exc, _sibling(INSTALL_SKILLS).ClaimRestoreError):
1537
+ rollback_errors.append(str(exc))
1450
1538
  if profile.get("target") and "APPLIED: " in profile_output:
1451
1539
  restore_preimage = profile_preimage
1452
1540
  if backup_match:
@@ -1483,6 +1571,7 @@ def main(argv: list[str] | None = None) -> int:
1483
1571
  remove_exact_created_links(destination, new_skills)
1484
1572
  except OSError as rollback_exc:
1485
1573
  rollback_errors.append(f"skill rollback failed: {rollback_exc}")
1574
+ rollback_errors.extend(restore_retired_links(retired_claims))
1486
1575
  remaining_links = [
1487
1576
  name
1488
1577
  for name in new_skills
@@ -1507,6 +1596,7 @@ def main(argv: list[str] | None = None) -> int:
1507
1596
  detail = f"{exc}; rollback incomplete: {'; '.join(rollback_errors)}" if rollback_errors else f"{exc}; applied changes were rolled back and read back"
1508
1597
  print(f"ERROR: setup state save failed: {detail}", file=sys.stderr)
1509
1598
  return 3 if rollback_errors else 2
1599
+ commit_retired_links(retired_claims)
1510
1600
  after = installed_ids(destination)
1511
1601
  actual_added = sorted(after - before)
1512
1602
  expected_added = sorted(BY_SKILL[name].id for name in new_skills)
@@ -18,7 +18,7 @@ LINK_RE = re.compile(r"\[[^\]]+\]\((?!https?://|#|mailto:)([^)]+)\)")
18
18
 
19
19
  SUITE = {
20
20
  "nobrainer-auto-fine-tune": "nb-auto-fine-tune",
21
- "nobrainer-codex-context": "nb-codex-context",
21
+ "nobrainer-context": "nb-context",
22
22
  "nobrainer-skill-doctor": "nb-skill-doctor",
23
23
  "nobrainer-tech-flow": "NBFlow",
24
24
  "nobrainer-team": "nb-team",
@@ -69,6 +69,7 @@ LEGACY = {
69
69
  "nb-write",
70
70
  "nobrainer-autopilot",
71
71
  "nobrainer-capture-lesson",
72
+ "nobrainer-codex-context",
72
73
  "nobrainer-continuous-improvement",
73
74
  "nobrainer-memory",
74
75
  "nobrainer-memory-memsearch",
@@ -37,7 +37,9 @@ Apply these principles as decision rules, not slogans:
37
37
 
38
38
  - `KISS`: choose the simplest complete design that satisfies acceptance.
39
39
  - `YAGNI`: do not add extension points, compatibility layers or options for an
40
- unproven future requirement.
40
+ unproven future requirement. Before adding complexity for a speculative race
41
+ or security edge case, name the concrete failure, its likelihood and the cost;
42
+ add it only when that case is accepted.
41
43
  - `DRY`: keep each rule and mutable fact under one owner, but do not abstract
42
44
  incidental similarity. Duplication can be cheaper than the wrong coupling.
43
45
  - `SOLID`: preserve cohesive responsibilities and stable dependency boundaries;
@@ -80,7 +82,9 @@ Reject a patch that contains any of the following without explicit acceptance:
80
82
  - duplicate state owners, speculative abstractions or fallback stacks that make
81
83
  failure ambiguous;
82
84
  - tests that only assert a mock/fixture while the real contract remains
83
- unexercised.
85
+ unexercised;
86
+ - in user interfaces, taglines, helper text and secondary labels added to explain
87
+ a screen; if the screen needs them, fix the layout.
84
88
 
85
89
  Names should explain intent. Comments should explain only non-obvious why,
86
90
  constraints, risk or tradeoffs. Do not create a new README, framework or process
@@ -0,0 +1,113 @@
1
+ ---
2
+ name: nobrainer-context
3
+ description: "Use when the owner says nb-context or asks to set up, audit or tidy one project's instructions for coding agents (AGENTS.md, CLAUDE.md, GEMINI.md, Cursor rules, Copilot instructions): find what each client loads and its size against documented limits, report duplicates and contradictions, then propose one source of truth with short pointers, applied only after approval and read back."
4
+ ---
5
+
6
+ # NoBrainer Context
7
+
8
+ Keep one tidy set of project instructions that every coding agent in use reads.
9
+ This module owns the project-instruction boundary of one repository: which files
10
+ each client loads, what they cost, where they repeat or disagree, and one
11
+ consolidation change. It does not own model selection, global client settings,
12
+ session transfer, goal tracking or the delivery workflow.
13
+
14
+ ## Select a mode
15
+
16
+ - `inspect` (default) - inventory and report. Do not write.
17
+ - `consolidate` - inspect, propose one source of truth with pointers, show the
18
+ exact preview, wait for explicit approval, apply project-local edits only,
19
+ then inspect again.
20
+ - `repair` - fix one known defect, such as a duplicate managed block, a dangling
21
+ import or a truncated Codex chain. Do not rewrite unrelated instructions.
22
+
23
+ A question gets `inspect`. Use `consolidate` only when the owner asks to tidy or
24
+ set up the project's instructions. Findings are evidence to report, not
25
+ permission to edit.
26
+
27
+ ## 1. Inventory what each client loads
28
+
29
+ Run the bundled read-only probe from the project with Python 3.11+, replacing
30
+ `<context-skill-dir>` with this module's directory:
31
+
32
+ ```bash
33
+ python3 <context-skill-dir>/scripts/context_inventory.py --json
34
+ ```
35
+
36
+ It lists, per client (Codex, Claude Code, Gemini CLI, Cursor, GitHub Copilot,
37
+ OpenCode), the files loaded at start, on demand or by import, with bytes, lines
38
+ and words; the Codex byte budget and truncation; files over documented line
39
+ guidance; byte-identical copies; lines repeated across files; imports of missing
40
+ files; settings or Codex configuration it cannot parse; and the Flow goal pointer
41
+ state. Instructions in folders below the current one are listed as on demand and
42
+ left out of the start-up totals. Add `--include-global` to include each client's
43
+ user-level file and, for Claude and Gemini, its recursive imports in the same
44
+ size, duplicate and goal-pointer checks. Claude's 200-line guidance applies only
45
+ to `CLAUDE.md` files discovered directly, not imported files, `AGENTS.md` fallbacks
46
+ or `.claude/rules/*.md` rules. Use `--client NAME` to narrow the report and
47
+ `--goal-file PATH` for an explicit goal file.
48
+ `scripts/codex_context.py --check --json` keeps the detailed Codex chain probe.
49
+
50
+ The probe applies the rules in [clients.md](references/clients.md). Only those
51
+ documented behaviours count; anything marked `UNVERIFIED` there is reported as
52
+ unknown, never as a limit. Ask which clients the team actually uses when the
53
+ files alone do not say, and drop the others from the report.
54
+
55
+ ## 2. Report duplicates and contradictions
56
+
57
+ Repeated lines come from the probe. Contradictions need reading: compare the
58
+ files a client loads together for different commands, package managers, test
59
+ gates, branch rules, languages or tone, and for a file that tells the agent to
60
+ read another file in words instead of importing it. Report each with exact
61
+ `file:line` pairs. This step is report-only.
62
+
63
+ ## 3. Propose one source of truth
64
+
65
+ Follow [consolidate.md](references/consolidate.md). The usual shape is
66
+ `AGENTS.md` as the source of truth, `CLAUDE.md` holding `@AGENTS.md` plus only
67
+ Claude-specific lines, and each other client's file reduced to a pointer or to
68
+ the lines that client alone needs. Show the full preview (new bytes or a diff
69
+ per file), what each client will load afterwards and the rollback, then wait
70
+ for explicit approval. Apply only inside the project. Never edit global
71
+ instruction files, `~/.codex/config.toml`, `~/.gemini/settings.json`, client
72
+ settings, credentials or plugin installs unless the owner asks for that
73
+ separate action.
74
+
75
+ ## 4. Read back
76
+
77
+ After a change, run the probe again and compare it with the preview. That is a
78
+ file-level readback. Mark `RUNTIME_READBACK: VERIFIED` only from a client's own
79
+ output that lists loaded files in a fresh session, as listed per client in
80
+ [clients.md](references/clients.md); otherwise `UNKNOWN`. A probe exit code or
81
+ an installed module does not prove a client followed the instructions.
82
+
83
+ ## 5. Goal pointer
84
+
85
+ When the project keeps a Flow goal file (`GOAL_ID:` block, see
86
+ [long-run state](../nobrainer-tech-flow/references/long-run-state.md)), check
87
+ that the instructions name the current one and no stale or missing goal path.
88
+ Propose the one-line pointer fix in the same preview. Goal content, status and
89
+ handoff stay with `nobrainer-tech-flow` and `nobrainer-sessions`.
90
+ If no active goals remain, the pointer state is `NONE`, including when completed
91
+ goals are still mentioned; a pointer to a missing file remains `STALE_POINTER`.
92
+
93
+ ## Closeout
94
+
95
+ ```text
96
+ CONTEXT: CURRENT | ATTENTION | UPDATED | BLOCKED
97
+ PROJECT_ROOT: <path or UNKNOWN>
98
+ CLIENTS: <client: loaded files and bytes, one per line>
99
+ LIMITS: <truncated or over-guidance files, or NONE>
100
+ DUPLICATES: <count and top file:line pairs, or NONE>
101
+ CONTRADICTIONS: <file:line pairs, or NONE>
102
+ SOURCE_OF_TRUTH: <file, or NOT_PROPOSED>
103
+ GOAL_POINTER: NONE | POINTED | NOT_POINTED | STALE_POINTER
104
+ PROJECT_WRITE: NONE | <files changed>
105
+ RUNTIME_READBACK: VERIFIED | UNKNOWN | UNSUPPORTED
106
+ OWNER_ACTION_REQUIRED: <one exact action or NONE>
107
+ ROLLBACK: <exact preimages or NOT_APPLICABLE>
108
+ ```
109
+
110
+ Route audits across many projects or skill portfolios to
111
+ `nobrainer-skill-doctor`, broader setup and delivery to `nobrainer-tech-flow`,
112
+ context transfer to `nobrainer-sessions` and release closeout to
113
+ `nobrainer-review`.
@@ -0,0 +1,115 @@
1
+ # What each client loads
2
+
3
+ Checked against official documentation on 2026-10-20. Client behaviour changes;
4
+ re-check the cited page before relying on a number, and report anything newer as
5
+ fresh evidence. `UNVERIFIED` means the official pages read did not document it:
6
+ treat it as unknown, never as a limit. `context_inventory.py` implements the
7
+ project-local rules below.
8
+
9
+ ## Codex
10
+
11
+ - Per directory from the project root (usually the Git root) down to the
12
+ working directory, at most one file: `AGENTS.override.md`, else `AGENTS.md`,
13
+ else the first name in `project_doc_fallback_filenames`. Files are joined
14
+ root first, so closer files appear later and win.
15
+ - Global: `AGENTS.override.md` or `AGENTS.md` in `CODEX_HOME` (default
16
+ `~/.codex`).
17
+ - Limit: `project_doc_max_bytes`, default 32 KiB; files past the limit are not
18
+ added. `project_root_markers` changes root detection; its default is
19
+ `UNVERIFIED` (the probe assumes `.git`).
20
+ - Imports: none documented.
21
+ - Readback: no command that prints the loaded instructions is documented
22
+ (`UNVERIFIED`); a model asked to list its sources is not a deterministic
23
+ readback. Use `codex_context.py` for the file chain.
24
+ - Sources: <https://developers.openai.com/codex/guides/agents-md>,
25
+ <https://developers.openai.com/codex/config-reference>.
26
+
27
+ ## Claude Code
28
+
29
+ - Project: `CLAUDE.md` or `.claude/CLAUDE.md`, and `CLAUDE.local.md` (personal,
30
+ gitignored), in the working directory and every directory above it, joined
31
+ root first; subdirectory files load when Claude works there.
32
+ - Rules: `.claude/rules/**/*.md`. A rule without `paths:` frontmatter loads at
33
+ start; with `paths:` it loads only for matching files.
34
+ - `AGENTS.md`: read directly (v2.1.277 or later) only when no `CLAUDE.md`,
35
+ `.claude/CLAUDE.md` or `CLAUDE.local.md` exists in the working directory or
36
+ above. Adding any of them stops that unless the user-level setting
37
+ `claude-md-and-agents-md` is chosen. A `CLAUDE.md` with `@AGENTS.md` works on
38
+ every version and never reads `AGENTS.md` twice.
39
+ - Imports: `@path`, relative to the importing file, up to four hops; imported
40
+ files inherit the importing file's start-up or on-demand scope. With
41
+ `--include-global`, the probe also expands imports from the user-level
42
+ `~/.claude/CLAUDE.md`. Not expanded inside code spans or blocks.
43
+ - Limits: guidance of under 200 lines per `CLAUDE.md`; the probe applies it to
44
+ directly discovered `CLAUDE.md` files, not imported files, `AGENTS.md` fallbacks
45
+ or `.claude/rules/*.md` rules. A file over 4 MiB is skipped.
46
+ - Readback: `/context` (Memory files) and `/memory` in a fresh interactive
47
+ session; the `InstructionsLoaded` hook logs loads (not for `AGENTS.md` read
48
+ through the setting). `/doctor prompt-audit` (v2.1.283 or later) is a native
49
+ report of stale or conflicting instructions. No non-interactive command is
50
+ documented.
51
+ - Source: <https://code.claude.com/docs/en/memory>.
52
+
53
+ ## Gemini CLI
54
+
55
+ - `~/.gemini/GEMINI.md`, then `GEMINI.md` in the workspace directories and
56
+ their parents, then files found when a tool reads a directory. All are joined.
57
+ - `context.fileName` in `settings.json` sets one name or a list, for example
58
+ `["AGENTS.md", "GEMINI.md"]`. The probe reads only project
59
+ `.gemini/settings.json`.
60
+ - Imports: `@path`, default depth 5, circular imports blocked. With
61
+ `--include-global`, the probe also expands imports from the user-level
62
+ `~/.gemini/GEMINI.md`, using the same size, duplicate and goal-pointer checks.
63
+ - Limit: none documented.
64
+ - Readback: `/memory show` prints the combined memory.
65
+ - Sources: <https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/gemini-md.md>,
66
+ <https://github.com/google-gemini/gemini-cli/blob/main/docs/reference/memport.md>.
67
+
68
+ ## Cursor
69
+
70
+ - Project rules: `.cursor/rules`, `.mdc` files only (a plain `.md` there is
71
+ ignored), with `description`, `globs` and `alwaysApply` frontmatter.
72
+ `alwaysApply: true` applies to every request; the others attach by file
73
+ match, agent choice or manual mention.
74
+ - `AGENTS.md` in the root and subdirectories; nested files combine with their
75
+ parents and the more specific one wins.
76
+ - `.cursorrules`: not mentioned in the current docs (`UNVERIFIED`); the probe
77
+ reports it as legacy.
78
+ - Limit: guidance to keep each rule under 500 lines.
79
+ - Readback: none documented.
80
+ - Sources: <https://cursor.com/docs/context/rules>,
81
+ <https://cursor.com/docs/cli/using>.
82
+
83
+ ## GitHub Copilot
84
+
85
+ - `.github/copilot-instructions.md` (repository-wide) and
86
+ `.github/instructions/**/*.instructions.md` (path-specific, `applyTo`).
87
+ - Agent files: `AGENTS.md` (nearest wins) and a root `CLAUDE.md` or `GEMINI.md`.
88
+ - Support differs by surface: the cloud agent, the CLI and code review on
89
+ GitHub.com read all three kinds; VS Code chat reads `AGENTS.md` but not
90
+ `CLAUDE.md` or `GEMINI.md`; Copilot Chat on GitHub.com and code review in VS
91
+ Code and Visual Studio read only `copilot-instructions.md`.
92
+ - Limit: none documented; a character limit for code review is `UNVERIFIED`.
93
+ - Readback: none documented.
94
+ - Sources: <https://docs.github.com/en/copilot/reference/custom-instructions-support>,
95
+ <https://docs.github.com/en/copilot/how-tos/configure-custom-instructions/add-repository-instructions>,
96
+ <https://code.visualstudio.com/docs/copilot/customization/custom-instructions>.
97
+
98
+ ## OpenCode
99
+
100
+ - Project: the first `AGENTS.md` found walking up from the working directory;
101
+ a `CLAUDE.md` only when no `AGENTS.md` exists. Global:
102
+ `~/.config/opencode/AGENTS.md`, else `~/.claude/CLAUDE.md`.
103
+ - `instructions` in the project `opencode.json`: extra paths, globs or URLs, all
104
+ added. Config files may be JSON or JSONC (JSON with comments). The docs name
105
+ `opencode.jsonc` for managed settings; a project `opencode.jsonc` is
106
+ `UNVERIFIED`, and the probe reads it too. The probe resolves local paths and
107
+ globs and never fetches URLs. Source: <https://opencode.ai/docs/config/>.
108
+ - Limit: none documented. Readback: none documented.
109
+ - Source: <https://opencode.ai/docs/rules>.
110
+
111
+ ## Other clients
112
+
113
+ The `AGENTS.md` format page lists further clients that read `AGENTS.md`
114
+ (<https://agents.md>). Treat any client not covered above as `UNVERIFIED` until
115
+ its own documentation is checked.
@@ -0,0 +1,66 @@
1
+ # One source of truth with pointers
2
+
3
+ Use this after an inventory, in `consolidate` or `repair` mode. Everything here
4
+ is project-local and needs a preview and explicit approval before any write.
5
+
6
+ ## Choose the source of truth
7
+
8
+ 1. Prefer the root `AGENTS.md`: Codex, Cursor, GitHub Copilot's agent surfaces,
9
+ OpenCode and Claude Code (directly or through an import) can read it.
10
+ 2. If the project has no `AGENTS.md`, propose moving the shared content of the
11
+ largest current file into one, unless the team uses a single client and
12
+ says it does not want another file.
13
+ 3. Keep nested `AGENTS.md` files that hold real directory-specific rules; Codex
14
+ and Cursor combine them with the root file, and closer files win.
15
+
16
+ ## Reduce the other files
17
+
18
+ | File | Keep | Pointer |
19
+ |---|---|---|
20
+ | `CLAUDE.md` | Claude-only lines | `@AGENTS.md` on its own line |
21
+ | `.claude/CLAUDE.md` | Claude-only lines | `@../AGENTS.md`: imports resolve from the importing file's folder |
22
+ | `CLAUDE.local.md` | personal, uncommitted lines | none; it stops Claude reading `AGENTS.md` directly, so keep the `@AGENTS.md` import in `CLAUDE.md` |
23
+ | `GEMINI.md` | Gemini-only lines | `@AGENTS.md`, or `context.fileName` in project `.gemini/settings.json` listing `AGENTS.md` |
24
+ | `.cursor/rules/*.mdc` | path- or task-scoped rules | none needed; Cursor reads `AGENTS.md` |
25
+ | `.github/copilot-instructions.md` | lines for surfaces that ignore `AGENTS.md` (Copilot Chat on GitHub.com, code review in IDEs) | no import syntax; keep it short and non-contradictory |
26
+ | `.github/instructions/*.instructions.md` | path-scoped rules with `applyTo` | none |
27
+ | `opencode.json` / `opencode.jsonc` `instructions` | extra files OpenCode should add | entries only |
28
+
29
+ Rules:
30
+
31
+ - Keep each fact in one file. Delete a duplicate only after the source of truth
32
+ holds the same meaning; when the copies disagree, report the conflict and ask
33
+ which wording wins instead of picking one.
34
+ - An import is not free: Claude Code and Gemini CLI load imported files at
35
+ start, so the import moves bytes, it does not remove them.
36
+ - A sentence that only tells an agent to read another file is unreliable; use
37
+ the client's import or file-name setting when it has one.
38
+ - A byte-identical copy (for example `CLAUDE.md` copied from `AGENTS.md`)
39
+ drifts, and GitHub Copilot's agent surfaces read both copies. Replace it with
40
+ the pointer unless the project requires the copy; a test that enforces the
41
+ copy is such a reason.
42
+ - Preserve client-managed blocks (for example `NOBRAINER-TECH-FLOW` markers),
43
+ `AGENTS.override.md` precedence and every byte outside the lines you change.
44
+ A `NOBRAINER-CODEX-CONTEXT` block left by an earlier release is ordinary
45
+ project content: keep it, or fold its facts into the source of truth and
46
+ remove the markers in the same approved preview.
47
+ - Never create a file a client does not read and call it loaded. A Codex
48
+ fallback name works only when `project_doc_fallback_filenames` lists it;
49
+ changing that global setting is a separate owner action.
50
+ - Keep credentials, private machine paths, mutable runtime state, guessed
51
+ commands and model names out of instruction files.
52
+
53
+ ## Preview
54
+
55
+ Show, for every file: the current bytes, the proposed bytes or a unified diff,
56
+ and what each client in use will load afterwards with byte counts. Name the
57
+ rollback: the exact preimage of each changed file (or a recoverable copy) and
58
+ the files to delete if new. Wait for an explicit yes.
59
+
60
+ ## Apply and read back
61
+
62
+ Write only the approved files, then run the inventory again. The change is
63
+ `UPDATED` when the new inventory matches the preview: the same files per client,
64
+ no new truncation, no import of a missing file, fewer or equal repeated lines and
65
+ the goal pointer as proposed. Otherwise restore the preimages and report `BLOCKED` with the
66
+ difference.