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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +14 -8
- package/docs/INSTALL.md +36 -22
- package/docs/MIGRATION_TO_FLOW.md +14 -0
- package/docs/SKILL_CURATION.md +8 -2
- package/docs/releases/v2.5.0.md +190 -0
- package/docs/releases/v2.6.0.md +73 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/install.py +16 -7
- package/scripts/install.sh +129 -17
- package/scripts/install_personalization.py +51 -16
- package/scripts/install_skills.py +34 -5
- package/scripts/recommend_flow_setup.py +102 -12
- package/scripts/validate_skills.py +2 -1
- package/skills/nobrainer-build/SKILL.md +6 -2
- package/skills/nobrainer-context/SKILL.md +113 -0
- package/skills/nobrainer-context/references/clients.md +115 -0
- package/skills/nobrainer-context/references/consolidate.md +66 -0
- package/skills/nobrainer-context/scripts/context_inventory.py +425 -0
- package/skills/nobrainer-skill-doctor/SKILL.md +4 -4
- package/skills/nobrainer-tech-flow/SKILL.md +3 -3
- package/skills/nobrainer-tech-flow/VERSION +1 -1
- package/skills/nobrainer-tech-flow/references/daily-update.md +5 -2
- package/skills/nobrainer-tech-flow/references/delivery.md +17 -2
- package/skills/nobrainer-tech-flow/references/optional-decisions.md +5 -3
- package/skills/nobrainer-tech-flow/references/routing.md +1 -1
- package/skills/nobrainer-tech-flow/scripts/typed_decisions.py +20 -9
- package/skills/nobrainer-codex-context/SKILL.md +0 -111
- /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-
|
|
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
|
|
686
|
+
if not recorded_skill(name):
|
|
608
687
|
continue
|
|
609
688
|
target = destination / name
|
|
610
|
-
if
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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.
|