@bglocation/tune-context 2.1.0 → 2.2.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/CHANGELOG.md +55 -0
- package/README.md +15 -0
- package/bin/tune-context.mjs +108 -11
- package/cli/generate.mjs +146 -12
- package/cli/sync-doctrine.mjs +28 -11
- package/cli/verify.mjs +116 -4
- package/package.json +1 -1
- package/skills/tune-context/SKILL.md +18 -4
- package/skills/tune-context/templates/CLAUDE.md.project.authored.tmpl +16 -0
- package/skills/tune-context/templates/CLAUDE.md.project.tmpl +7 -16
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "tune-context",
|
|
11
11
|
"source": "./",
|
|
12
12
|
"description": "Context-efficiency configurator for Claude Code: doctrine skills (token-efficiency, caveman, cdd, phase-workflow) plus a tune-context configurator that scaffolds a lean CLAUDE.md, subagents, settings and hooks.",
|
|
13
|
-
"version": "2.
|
|
13
|
+
"version": "2.2.0",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "Szymon Walczak",
|
|
16
16
|
"url": "https://bglocation.dev"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "tune-context",
|
|
3
3
|
"displayName": "tune-context",
|
|
4
4
|
"description": "Context-efficiency configurator for Claude Code: doctrine skills (token-efficiency, caveman, cdd, phase-workflow) plus a tune-context configurator that scaffolds a lean CLAUDE.md, subagents, settings and hooks.",
|
|
5
|
-
"version": "2.
|
|
5
|
+
"version": "2.2.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Szymon Walczak",
|
|
8
8
|
"url": "https://bglocation.dev"
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,57 @@ All notable changes to `@bglocation/tune-context` are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.2.0] — 2026-07-27
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **The managed block no longer holds sections you are meant to write yourself.**
|
|
13
|
+
`## Architecture`, `## Conventions that bite` and the one-line summary used to
|
|
14
|
+
sit *inside* the markers behind `<!-- fill in: … -->` prompts — so the tool
|
|
15
|
+
invited you to write in the one place it overwrites, and re-running it replaced
|
|
16
|
+
a repo's own documentation with placeholders. Measured on this repo before the
|
|
17
|
+
fix: 36 lines replaced, every one inside the block. Those sections are now
|
|
18
|
+
seeded **below** the closing marker, once, from a new
|
|
19
|
+
`CLAUDE.md.project.authored.tmpl`, and are never regenerated. The block keeps
|
|
20
|
+
only detected facts: project name, build/test/lint, MCP rules, doctrine pointer.
|
|
21
|
+
Affects both channels — the CLI and the `/tune-context` skill share the template.
|
|
22
|
+
|
|
23
|
+
- **`verify` no longer passes an install it did not actually check.** Deployed
|
|
24
|
+
hooks, agents and skills are now compared by content against the installed
|
|
25
|
+
package, so a copy left behind by an earlier install is reported by name.
|
|
26
|
+
Skills compare recursively, so an edit inside `skills/tune-context/templates/`
|
|
27
|
+
counts too. Agents and the `tune-context` skill had **no check at all** before.
|
|
28
|
+
Previously `verify` asserted only that a hook file existed and was executable
|
|
29
|
+
— which is how 2.1.0 shipped with the CLI reporting 2.1.0, every check green,
|
|
30
|
+
and the two-releases-old `context-reminder.mjs` still deployed.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **`tune-context verify`** — a read-only command that reports on an existing
|
|
35
|
+
install without touching anything. It exists because `init` *cannot* do this
|
|
36
|
+
job: init re-syncs before it verifies, so the drift an upgrade leaves behind
|
|
37
|
+
is already healed by the time init's own verify step looks. Run it after
|
|
38
|
+
upgrading. `init` now also names what it refreshed, instead of healing silently.
|
|
39
|
+
- A `?` verdict, distinct from ✓ and ✗, for checks that could not be observed —
|
|
40
|
+
currently "no installed package to compare against" and "nothing from this
|
|
41
|
+
package is deployed here" (the plugin channel). It neither passes nor fails.
|
|
42
|
+
- **A guard for files still in the old layout.** `init` now classifies an existing
|
|
43
|
+
`CLAUDE.md` before writing. If authored text sits inside the managed block, it
|
|
44
|
+
writes **nothing** to that file, finishes every other step (hooks, settings,
|
|
45
|
+
doctrine), and prints what to move and where. A block whose slots still hold the
|
|
46
|
+
untouched placeholders migrates silently — nothing to lose there.
|
|
47
|
+
- `tune-context init --force-claude-md` — regenerate a guarded file anyway,
|
|
48
|
+
discarding the in-block content. Opt-in only; without it, such a file could
|
|
49
|
+
never be migrated by the tool at all.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- Migrating an old `CLAUDE.md` no longer seeds a **second, empty** copy of a
|
|
54
|
+
section the file already carries below the marker. The likeliest legacy shape
|
|
55
|
+
is a block whose slots were never filled precisely because their owner took the
|
|
56
|
+
advice and wrote below it; seeding on top of that lost nothing, but left a
|
|
57
|
+
curated file messier than it was found.
|
|
58
|
+
|
|
8
59
|
## [2.1.0] — 2026-07-26
|
|
9
60
|
|
|
10
61
|
### Changed
|
|
@@ -329,6 +380,10 @@ action needed on upgrade.
|
|
|
329
380
|
(`explore-cheap`, `reviewer`), the PreCompact / SessionStart / PostToolUse hook
|
|
330
381
|
suite, and the adoption report (`eval/adoption-report.mjs`).
|
|
331
382
|
|
|
383
|
+
[2.2.0]: https://gitlab.com/bglocation/tune-context/-/tags/v2.2.0
|
|
384
|
+
[2.1.0]: https://gitlab.com/bglocation/tune-context/-/tags/v2.1.0
|
|
385
|
+
[2.0.1]: https://gitlab.com/bglocation/tune-context/-/tags/v2.0.1
|
|
386
|
+
[2.0.0]: https://gitlab.com/bglocation/tune-context/-/tags/v2.0.0
|
|
332
387
|
[1.1.0]: https://gitlab.com/bglocation/tune-context/-/tags/v1.1.0
|
|
333
388
|
[1.0.1]: https://gitlab.com/bglocation/tune-context/-/tags/v1.0.1
|
|
334
389
|
[1.0.0]: https://gitlab.com/bglocation/tune-context/-/tags/v1.0.0
|
package/README.md
CHANGED
|
@@ -39,6 +39,21 @@ That single command detects the stack + MCP servers, writes a lean `CLAUDE.md`,
|
|
|
39
39
|
syncs the doctrine skills/agents/hooks into `~/.claude/`, merges hooks and
|
|
40
40
|
permissions, and prints a verification report. It's idempotent — safe to re-run.
|
|
41
41
|
|
|
42
|
+
Safe to re-run means exactly this: the managed block in `CLAUDE.md` holds only
|
|
43
|
+
what detection produced and is rewritten every time; everything below the closing
|
|
44
|
+
marker is yours and is never touched, including the `## Architecture` and
|
|
45
|
+
`## Conventions that bite` sections the tool seeds there once. Write your own
|
|
46
|
+
notes below the marker, not inside the block.
|
|
47
|
+
|
|
48
|
+
**After upgrading the package, re-run `init`.** `npm i -g` updates the package,
|
|
49
|
+
not the hooks an earlier run copied into `~/.claude/hooks/` — `settings.json`
|
|
50
|
+
points at those copies, so a release that changes hooks goes live only after
|
|
51
|
+
another `init`. To check without changing anything:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
tune-context verify # read-only: is what's deployed still what ships?
|
|
55
|
+
```
|
|
56
|
+
|
|
42
57
|
Install it globally if you'll use it across repos:
|
|
43
58
|
|
|
44
59
|
```bash
|
package/bin/tune-context.mjs
CHANGED
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
mergeHooks,
|
|
30
30
|
mergePermissions,
|
|
31
31
|
} from '../cli/generate.mjs';
|
|
32
|
-
import { verify } from '../cli/verify.mjs';
|
|
32
|
+
import { staleDoctrineEntries, verify } from '../cli/verify.mjs';
|
|
33
33
|
|
|
34
34
|
const HOOK_EVENTS = [
|
|
35
35
|
{ event: 'PreCompact', scriptName: 'pre-compact-snapshot.mjs' },
|
|
@@ -56,9 +56,19 @@ function usage() {
|
|
|
56
56
|
return `tune-context init — configure this repo for token-efficient Claude Code use.
|
|
57
57
|
|
|
58
58
|
Usage:
|
|
59
|
-
tune-context init [--cwd <dir>]
|
|
59
|
+
tune-context init [--cwd <dir>] [--force-claude-md]
|
|
60
|
+
tune-context verify [--cwd <dir>]
|
|
60
61
|
tune-context --help
|
|
61
62
|
tune-context --version
|
|
63
|
+
|
|
64
|
+
verify Check an existing install without changing anything —
|
|
65
|
+
including whether the deployed hooks/skills/agents still
|
|
66
|
+
match this package. Run it after upgrading: an upgrade
|
|
67
|
+
does not refresh copies an earlier init made.
|
|
68
|
+
|
|
69
|
+
--force-claude-md Regenerate a project CLAUDE.md whose hand-written content
|
|
70
|
+
still sits inside the managed block. Discards that content.
|
|
71
|
+
Without it, such a file is left untouched.
|
|
62
72
|
`;
|
|
63
73
|
}
|
|
64
74
|
|
|
@@ -73,14 +83,21 @@ export function parseArgs(argv) {
|
|
|
73
83
|
// tool, not an operation on a repo, so it must not require a subcommand.
|
|
74
84
|
if (argv.includes('--version') || argv.includes('-v')) return { mode: 'version' };
|
|
75
85
|
const [command, ...rest] = argv;
|
|
76
|
-
|
|
86
|
+
// `verify` joins `init` as an accepted command (TASK-081). It writes nothing,
|
|
87
|
+
// which is the point: init syncs before it verifies, so init can never
|
|
88
|
+
// observe the drift an upgrade leaves behind — only a check that runs on its
|
|
89
|
+
// own can. The TASK-078 rule still holds: an unknown command fails loudly.
|
|
90
|
+
if (command !== 'init' && command !== 'verify') {
|
|
77
91
|
return {
|
|
78
92
|
mode: 'error',
|
|
79
|
-
message: command === undefined ? 'Missing command: init' : `Unknown command: ${command}`,
|
|
93
|
+
message: command === undefined ? 'Missing command: init or verify' : `Unknown command: ${command}`,
|
|
80
94
|
};
|
|
81
95
|
}
|
|
96
|
+
// Opt-in only, and never inferred: this is the one flag that can destroy
|
|
97
|
+
// content the tool otherwise refuses to touch (TASK-082).
|
|
98
|
+
const forceClaudeMd = rest.includes('--force-claude-md');
|
|
82
99
|
const cwdFlagIdx = rest.indexOf('--cwd');
|
|
83
|
-
if (cwdFlagIdx === -1) return { mode:
|
|
100
|
+
if (cwdFlagIdx === -1) return { mode: command, cwd: process.cwd(), forceClaudeMd };
|
|
84
101
|
const value = rest[cwdFlagIdx + 1];
|
|
85
102
|
// Undefined (last arg) or another flag (e.g. `--cwd --foo`) both mean "no
|
|
86
103
|
// value was actually given" — resolve(undefined) used to throw a raw
|
|
@@ -88,10 +105,10 @@ export function parseArgs(argv) {
|
|
|
88
105
|
if (value === undefined || value.startsWith('-')) {
|
|
89
106
|
return { mode: 'error', message: 'Missing value for --cwd' };
|
|
90
107
|
}
|
|
91
|
-
return { mode:
|
|
108
|
+
return { mode: command, cwd: resolve(value), forceClaudeMd };
|
|
92
109
|
}
|
|
93
110
|
|
|
94
|
-
function runInit(cwd) {
|
|
111
|
+
function runInit(cwd, { forceClaudeMd = false } = {}) {
|
|
95
112
|
const templatesDir = join(pkgRoot, 'skills', 'tune-context', 'templates');
|
|
96
113
|
const { claudeDir } = claudeDirs();
|
|
97
114
|
|
|
@@ -115,9 +132,16 @@ function runInit(cwd) {
|
|
|
115
132
|
|
|
116
133
|
const detection = { stack, mcpServers };
|
|
117
134
|
|
|
135
|
+
// Sample staleness BEFORE syncing. init's own sync heals drift, so by the
|
|
136
|
+
// time verify runs at the end there is nothing left to find — the whole
|
|
137
|
+
// reason `tune-context verify` exists as a command of its own (TASK-081).
|
|
138
|
+
// What init can still do, and what an upgrading user needs to hear, is name
|
|
139
|
+
// what WAS stale and has just been refreshed.
|
|
140
|
+
const wasStale = staleDoctrineEntries(claudeDir, pkgRoot)?.stale ?? [];
|
|
141
|
+
|
|
118
142
|
const synced = installDoctrine(pkgRoot, claudeDir);
|
|
119
143
|
const userClaudeMd = generateUserClaudeMd(claudeDir, templatesDir);
|
|
120
|
-
const projectClaudeMd = generateProjectClaudeMd(cwd, detection, templatesDir);
|
|
144
|
+
const projectClaudeMd = generateProjectClaudeMd(cwd, detection, templatesDir, { force: forceClaudeMd });
|
|
121
145
|
|
|
122
146
|
const settingsTemplate = loadSettingsTemplate(templatesDir);
|
|
123
147
|
const hooksAdded = mergeHooks(existing.userSettingsJson, { hooks: settingsTemplate.hooks });
|
|
@@ -130,6 +154,7 @@ function runInit(cwd) {
|
|
|
130
154
|
projectSettingsPath: existing.projectSettingsJson,
|
|
131
155
|
expectedHookCommands: HOOK_EVENTS,
|
|
132
156
|
expectedPermissions: permissionsAllow,
|
|
157
|
+
pkgRoot,
|
|
133
158
|
});
|
|
134
159
|
|
|
135
160
|
console.log(`tune-context init — ${cwd}\n`);
|
|
@@ -138,8 +163,20 @@ function runInit(cwd) {
|
|
|
138
163
|
console.log(' build/test/lint commands not detected for this ecosystem — filled with TODO placeholders in CLAUDE.md.');
|
|
139
164
|
}
|
|
140
165
|
console.log(`\nDoctrine synced to ${claudeDir}: ${synced.length} item(s)`);
|
|
166
|
+
if (wasStale.length) {
|
|
167
|
+
const noun = wasStale.length === 1 ? 'copy' : 'copies';
|
|
168
|
+
console.log(` refreshed ${wasStale.length} stale ${noun} left behind by an earlier install: ${wasStale.join(', ')}`);
|
|
169
|
+
}
|
|
141
170
|
console.log(`User CLAUDE.md: ${userClaudeMd.changed ? 'written' : `skipped (${userClaudeMd.skipped})`}`);
|
|
142
|
-
|
|
171
|
+
if (projectClaudeMd.guarded) {
|
|
172
|
+
console.log(`Project CLAUDE.md: SKIPPED — ${projectClaudeMd.skipped}`);
|
|
173
|
+
} else {
|
|
174
|
+
// migrated is checked first: a legacy file whose owner already wrote their
|
|
175
|
+
// own sections below the marker migrates without being seeded, so `seeded`
|
|
176
|
+
// alone would report that run as a plain "updated".
|
|
177
|
+
const how = projectClaudeMd.migrated ? 'migrated to the new layout' : projectClaudeMd.seeded ? 'created' : 'updated';
|
|
178
|
+
console.log(`Project CLAUDE.md: ${projectClaudeMd.changed ? `${how} ${projectClaudeMd.path}` : 'already up to date'}`);
|
|
179
|
+
}
|
|
143
180
|
console.log(`Hooks merged into ${existing.userSettingsJson}: ${hooksAdded.length ? hooksAdded.join(', ') : 'already present'}`);
|
|
144
181
|
console.log(`Permissions merged into ${existing.projectSettingsJson}: ${permissionsAdded.length ? permissionsAdded.join(', ') : 'already present'}`);
|
|
145
182
|
console.log(`MCP servers detected: ${mcpServers.length ? mcpServers.map((s) => `${s.name} (${s.scope}, ${s.classification})`).join(', ') : 'none'}`);
|
|
@@ -148,13 +185,71 @@ function runInit(cwd) {
|
|
|
148
185
|
console.log(` skipped (rejected via disabledMcpjsonServers): ${rejectedServers.map((s) => s.name).join(', ')} — no permission rule, no CLAUDE.md line`);
|
|
149
186
|
}
|
|
150
187
|
|
|
188
|
+
// Loud, and separated from the one-line summary: this is the only step that
|
|
189
|
+
// finished with nothing written, and the user has to act for it to land.
|
|
190
|
+
if (projectClaudeMd.guarded) {
|
|
191
|
+
console.log(`
|
|
192
|
+
! ${projectClaudeMd.path} was NOT written.
|
|
193
|
+
It predates the current layout: "## Architecture" / "## Conventions that bite"
|
|
194
|
+
sit INSIDE the managed markers, where every run regenerates. Your text is
|
|
195
|
+
still there and untouched.
|
|
196
|
+
To migrate: move those sections BELOW the "${'<!-- <<< tune-context (managed) <<< -->'}"
|
|
197
|
+
line, then re-run init — everything outside the markers is never touched again.
|
|
198
|
+
To regenerate anyway and lose them: tune-context init --force-claude-md`);
|
|
199
|
+
}
|
|
200
|
+
|
|
151
201
|
console.log(`\nVerify: ${result.ok ? 'OK' : 'FAILED'}`);
|
|
152
202
|
for (const check of result.checks) {
|
|
153
|
-
|
|
203
|
+
// `?` is its own verdict: something we could not observe, reported as
|
|
204
|
+
// neither verified nor broken (TASK-081).
|
|
205
|
+
const mark = check.unknown ? '?' : check.ok ? '✓' : '✗';
|
|
206
|
+
console.log(` ${mark} ${check.name}${check.detail ? ` — ${check.detail}` : ''}`);
|
|
154
207
|
}
|
|
208
|
+
|
|
209
|
+
// Said on every run, not only when drift is found: the moment a user learns
|
|
210
|
+
// this is the moment they upgrade, and by then a stale hook is already live.
|
|
211
|
+
console.log(`\nNote: upgrading the package does not refresh hooks already installed in ${join(claudeDir, 'hooks')}.
|
|
212
|
+
Re-run \`tune-context init\` after any release that changes hooks/ — the staleness check above is what catches it if you don't.`);
|
|
213
|
+
|
|
155
214
|
if (!result.ok) process.exitCode = 1;
|
|
156
215
|
}
|
|
157
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Read-only counterpart to init: report on an install without touching a single
|
|
219
|
+
* file. Detection still runs, because the expected permissions depend on the
|
|
220
|
+
* project's own stack — but nothing is written, so this is safe to run anywhere,
|
|
221
|
+
* any number of times, including from CI after an upgrade.
|
|
222
|
+
*/
|
|
223
|
+
function runVerify(cwd) {
|
|
224
|
+
const { claudeDir } = claudeDirs();
|
|
225
|
+
const existing = detectExistingConfig(cwd);
|
|
226
|
+
const detection = { stack: detectStack(cwd), mcpServers: detectMcpServers(cwd) };
|
|
227
|
+
const permissionsAllow = buildPermissionsAllow(detection);
|
|
228
|
+
|
|
229
|
+
const result = verify({
|
|
230
|
+
claudeDir,
|
|
231
|
+
userSettingsPath: existing.userSettingsJson,
|
|
232
|
+
projectSettingsPath: existing.projectSettingsJson,
|
|
233
|
+
expectedHookCommands: HOOK_EVENTS,
|
|
234
|
+
expectedPermissions: permissionsAllow,
|
|
235
|
+
pkgRoot,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
console.log(`tune-context verify — ${cwd}\n`);
|
|
239
|
+
console.log(`Verify: ${result.ok ? 'OK' : 'FAILED'}`);
|
|
240
|
+
for (const check of result.checks) {
|
|
241
|
+
const mark = check.unknown ? '?' : check.ok ? '✓' : '✗';
|
|
242
|
+
console.log(` ${mark} ${check.name}${check.detail ? ` — ${check.detail}` : ''}`);
|
|
243
|
+
}
|
|
244
|
+
// A read-only command owes the reader the next step. Run on a machine that
|
|
245
|
+
// never ran init, this prints a screen of ✗ and, without the line below,
|
|
246
|
+
// nothing that says the whole list is one command away from green.
|
|
247
|
+
if (!result.ok) {
|
|
248
|
+
console.log('\nNext: `tune-context init` writes what is missing and refreshes what drifted. It never edits content outside the managed block.');
|
|
249
|
+
process.exitCode = 1;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
158
253
|
export function main() {
|
|
159
254
|
const parsed = parseArgs(process.argv.slice(2));
|
|
160
255
|
if (parsed.mode === 'help') {
|
|
@@ -168,8 +263,10 @@ export function main() {
|
|
|
168
263
|
// touched on an invalid invocation (TASK-078).
|
|
169
264
|
process.stderr.write(`${parsed.message}\n\n${usage()}`);
|
|
170
265
|
process.exitCode = 1;
|
|
266
|
+
} else if (parsed.mode === 'verify') {
|
|
267
|
+
runVerify(parsed.cwd);
|
|
171
268
|
} else {
|
|
172
|
-
runInit(parsed.cwd);
|
|
269
|
+
runInit(parsed.cwd, { forceClaudeMd: parsed.forceClaudeMd });
|
|
173
270
|
}
|
|
174
271
|
}
|
|
175
272
|
|
package/cli/generate.mjs
CHANGED
|
@@ -5,7 +5,37 @@
|
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
7
|
import { syncDoctrine } from './sync-doctrine.mjs';
|
|
8
|
-
import { upsertManagedBlock } from './managed-block.mjs';
|
|
8
|
+
import { END_MARKER, START_MARKER, upsertManagedBlock } from './managed-block.mjs';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Headings the PRE-TASK-082 template kept INSIDE the managed block, together
|
|
12
|
+
* with the exact placeholders the CLI wrote into them (old generate.mjs slots
|
|
13
|
+
* ONE_LINE_WHAT_THIS_IS / ARCHITECTURE_BULLETS / CONVENTIONS).
|
|
14
|
+
*
|
|
15
|
+
* They exist here for exactly one reason: recognising a file written by an
|
|
16
|
+
* older version. The current template emits neither, so "does the block still
|
|
17
|
+
* contain them" is a precise test for the old layout — no heuristics, no
|
|
18
|
+
* guessing whether prose "looks hand-written".
|
|
19
|
+
*
|
|
20
|
+
* This is the one place that deliberately encodes knowledge of a previous
|
|
21
|
+
* template. Deleting it would not break generation; it would break the guard
|
|
22
|
+
* that stops a regeneration from erasing someone's documentation.
|
|
23
|
+
*/
|
|
24
|
+
const LEGACY_AUTHORED_HEADINGS = ['## Architecture', '## Conventions that bite'];
|
|
25
|
+
const LEGACY_PLACEHOLDERS = [
|
|
26
|
+
'<!-- fill in: one sentence, what this project is -->',
|
|
27
|
+
'<!-- fill in: 3-6 lines, module -> responsibility -->',
|
|
28
|
+
'<!-- fill in: only rules that bite -->',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The headings the seed would introduce. If the file already carries one of them
|
|
33
|
+
* OUTSIDE the block it already has an authored half, and seeding would staple a
|
|
34
|
+
* second, empty copy above the one its owner actually wrote. A legacy file whose
|
|
35
|
+
* in-block slots were never filled *because* the owner wrote below the marker is
|
|
36
|
+
* exactly that case, and it is the likeliest one: they followed the advice.
|
|
37
|
+
*/
|
|
38
|
+
const SEEDED_HEADINGS = ['## What this is', ...LEGACY_AUTHORED_HEADINGS];
|
|
9
39
|
|
|
10
40
|
/**
|
|
11
41
|
* The hooks directory baked into settings.json commands.
|
|
@@ -86,9 +116,105 @@ function templateInnerBlock(templatePath) {
|
|
|
86
116
|
return raw.slice(startLineEnd, end).trimEnd();
|
|
87
117
|
}
|
|
88
118
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Which layout an existing project CLAUDE.md is in — the whole guard rests on
|
|
121
|
+
* this one classification.
|
|
122
|
+
*
|
|
123
|
+
* @param {string|null} existing - file content, or null if there is no file
|
|
124
|
+
* @returns {'absent'|'no-block'|'current'|'legacy-empty'|'legacy-authored'}
|
|
125
|
+
*
|
|
126
|
+
* absent - nothing there yet; create block + seed the authored part
|
|
127
|
+
* no-block - a hand-written CLAUDE.md with no markers; appending our
|
|
128
|
+
* block cannot touch what is already there
|
|
129
|
+
* current - post-TASK-082 layout; regenerate the block, nothing else
|
|
130
|
+
* legacy-empty - old layout, the authored slots still hold their untouched
|
|
131
|
+
* placeholders; migrating loses nothing, so do it silently
|
|
132
|
+
* legacy-authored - old layout with real content inside the block; the ONLY
|
|
133
|
+
* case that must not be overwritten
|
|
134
|
+
*
|
|
135
|
+
* "All three placeholders still present verbatim" is the test for legacy-empty,
|
|
136
|
+
* so any deviation — filled in, reworded, or a section deleted — classifies as
|
|
137
|
+
* authored. The asymmetry is deliberate: a needless guard costs one message, a
|
|
138
|
+
* missed one costs somebody's documentation.
|
|
139
|
+
*/
|
|
140
|
+
export function classifyProjectClaudeMd(existing) {
|
|
141
|
+
if (existing == null) return 'absent';
|
|
142
|
+
const start = existing.indexOf(START_MARKER);
|
|
143
|
+
const end = existing.indexOf(END_MARKER);
|
|
144
|
+
if (start === -1 || end === -1 || end < start) return 'no-block';
|
|
145
|
+
const block = existing.slice(start, end);
|
|
146
|
+
if (!LEGACY_AUTHORED_HEADINGS.some((heading) => block.includes(heading))) return 'current';
|
|
147
|
+
return LEGACY_PLACEHOLDERS.every((placeholder) => block.includes(placeholder)) ? 'legacy-empty' : 'legacy-authored';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The seeded half of the project CLAUDE.md. A bare ENOENT here would surface as
|
|
152
|
+
* a Node stacktrace mid-init; the same reasoning as templateInnerBlock's marker
|
|
153
|
+
* check — a broken install must say what is broken, not fail cryptically or,
|
|
154
|
+
* worse, quietly skip the half of the file the user is meant to write in.
|
|
155
|
+
*/
|
|
156
|
+
function readAuthoredTemplate(templatesDir) {
|
|
157
|
+
const path = join(templatesDir, 'CLAUDE.md.project.authored.tmpl');
|
|
158
|
+
if (!existsSync(path)) throw new Error(`missing template ${path} — the authored half of CLAUDE.md cannot be seeded`);
|
|
159
|
+
return readFileSync(path, 'utf8');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Put the seeded authored skeleton directly under the managed block, so the
|
|
164
|
+
* generated document reads top-to-bottom (derived facts, then your own notes)
|
|
165
|
+
* instead of appending it after whatever else the file already holds.
|
|
166
|
+
*/
|
|
167
|
+
function hasAuthoredSectionsOutsideBlock(content) {
|
|
168
|
+
const start = content.indexOf(START_MARKER);
|
|
169
|
+
const end = content.indexOf(END_MARKER);
|
|
170
|
+
const outside =
|
|
171
|
+
start === -1 || end === -1 || end < start ? content : content.slice(0, start) + content.slice(end + END_MARKER.length);
|
|
172
|
+
return SEEDED_HEADINGS.some((heading) => outside.includes(heading));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function insertAuthoredSeed(content, seed) {
|
|
176
|
+
const idx = content.indexOf(END_MARKER);
|
|
177
|
+
const body = seed.trim();
|
|
178
|
+
if (idx === -1) return `${content.trimEnd()}\n\n${body}\n`;
|
|
179
|
+
const cut = idx + END_MARKER.length;
|
|
180
|
+
const tail = content.slice(cut).replace(/^\n+/, '');
|
|
181
|
+
return tail ? `${content.slice(0, cut)}\n\n${body}\n\n${tail}` : `${content.slice(0, cut)}\n\n${body}\n`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Write/refresh the project CLAUDE.md.
|
|
186
|
+
*
|
|
187
|
+
* The managed block carries ONLY what detection produced. Sections a human
|
|
188
|
+
* writes (what this is, architecture, conventions) are seeded once, BELOW the
|
|
189
|
+
* block, and never rewritten — the same never-clobber guarantee this module
|
|
190
|
+
* already gives ~/.claude/CLAUDE.md and settings.json. Before TASK-082 those
|
|
191
|
+
* sections lived inside the block with "fill in" prompts, i.e. the tool invited
|
|
192
|
+
* you to write exactly where it overwrites.
|
|
193
|
+
*
|
|
194
|
+
* @param {{force?: boolean}} [options] - force:true regenerates a legacy-authored
|
|
195
|
+
* file anyway, discarding the in-block content. Only ever set from an explicit
|
|
196
|
+
* `--force-claude-md`, never by default: without an escape hatch a file that
|
|
197
|
+
* trips the guard could never be migrated by the tool at all.
|
|
198
|
+
* @returns {{path, changed, layout, guarded?, seeded?, migrated?, skipped?}}
|
|
199
|
+
*/
|
|
200
|
+
export function generateProjectClaudeMd(cwd, detection, templatesDir, { force = false } = {}) {
|
|
201
|
+
const path = join(cwd, 'CLAUDE.md');
|
|
202
|
+
const existing = existsSync(path) ? readFileSync(path, 'utf8') : null;
|
|
203
|
+
const layout = classifyProjectClaudeMd(existing);
|
|
204
|
+
|
|
205
|
+
// Guard BEFORE any write. A warning printed after the fact would be worthless:
|
|
206
|
+
// the content it warns about would already be gone.
|
|
207
|
+
if (layout === 'legacy-authored' && !force) {
|
|
208
|
+
return {
|
|
209
|
+
path,
|
|
210
|
+
changed: false,
|
|
211
|
+
layout,
|
|
212
|
+
guarded: true,
|
|
213
|
+
skipped: 'hand-written content sits inside the managed block — regenerating would erase it',
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const inner = templateInnerBlock(join(templatesDir, 'CLAUDE.md.project.tmpl'));
|
|
92
218
|
|
|
93
219
|
const mcpRules = detection.mcpServers.length
|
|
94
220
|
? detection.mcpServers.map((s) => `- \`${s.name}\` (${s.scope}, ${s.classification}) — see token-efficiency skill for the matching rule.`).join('\n')
|
|
@@ -96,21 +222,29 @@ export function generateProjectClaudeMd(cwd, detection, templatesDir) {
|
|
|
96
222
|
|
|
97
223
|
const filled = fillTemplate(inner, {
|
|
98
224
|
PROJECT_NAME: detection.stack.name || '<!-- fill in: project name -->',
|
|
99
|
-
ONE_LINE_WHAT_THIS_IS: '<!-- fill in: one sentence, what this project is -->',
|
|
100
225
|
BUILD_CMD: detection.stack.buildCmd || '# no build script detected — fill in or delete this line',
|
|
101
226
|
TEST_CMD: detection.stack.testCmd || '# no test script detected — fill in or delete this line',
|
|
102
227
|
LINT_CMD: detection.stack.lintCmd || '# no lint script detected — fill in or delete this line',
|
|
103
228
|
RUN_NOTES: detection.stack.monorepo ? '<!-- monorepo detected — note which package/workspace to run commands from -->' : '',
|
|
104
229
|
MCP_RULES: mcpRules,
|
|
105
|
-
ARCHITECTURE_BULLETS: '<!-- fill in: 3-6 lines, module -> responsibility -->',
|
|
106
|
-
CONVENTIONS: '<!-- fill in: only rules that bite -->',
|
|
107
230
|
});
|
|
108
231
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
232
|
+
const { content } = upsertManagedBlock(existing, filled);
|
|
233
|
+
|
|
234
|
+
// Seed exactly when the authored sections have nowhere else to live: a brand
|
|
235
|
+
// new file, or an old one whose never-filled sections vanish with the block
|
|
236
|
+
// they sat in — and, either way, only when the file does not already have an
|
|
237
|
+
// authored half of its own below the marker. Never on a `current` file: the
|
|
238
|
+
// user may have deleted those headings on purpose, and re-adding them would be
|
|
239
|
+
// its own kind of clobber.
|
|
240
|
+
const seeded =
|
|
241
|
+
(layout === 'absent' || layout === 'legacy-empty' || (force && layout === 'legacy-authored')) &&
|
|
242
|
+
!hasAuthoredSectionsOutsideBlock(content);
|
|
243
|
+
const next = seeded ? insertAuthoredSeed(content, readAuthoredTemplate(templatesDir)) : content;
|
|
244
|
+
|
|
245
|
+
const changed = existing === null || next !== existing;
|
|
246
|
+
if (changed) writeFileSync(path, next);
|
|
247
|
+
return { path, changed, layout, seeded, migrated: layout === 'legacy-empty' };
|
|
114
248
|
}
|
|
115
249
|
|
|
116
250
|
export function generateUserClaudeMd(claudeDir, templatesDir) {
|
package/cli/sync-doctrine.mjs
CHANGED
|
@@ -14,25 +14,42 @@ import { chmodSync, cpSync, existsSync, mkdirSync, readdirSync, rmSync } from 'n
|
|
|
14
14
|
import { dirname, join } from 'node:path';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
+
* Everything this package deploys into a Claude config dir, as
|
|
18
|
+
* `{kind, name, source}` — one enumeration, used both by the copier below and
|
|
19
|
+
* by verify's staleness check (TASK-081). Two lists would drift apart, and a
|
|
20
|
+
* freshness check that walks a *different* set than the sync did would go
|
|
21
|
+
* green on exactly the file nobody thought to add to it.
|
|
22
|
+
*
|
|
17
23
|
* @param {string} pkgRoot - root of the tune-context package (contains skills/, agents/, hooks/)
|
|
18
|
-
* @
|
|
19
|
-
* @returns {{kind: string, name: string, dest: string}[]} synced items
|
|
24
|
+
* @returns {{kind: string, name: string, source: string}[]}
|
|
20
25
|
*/
|
|
21
|
-
export function
|
|
22
|
-
const
|
|
26
|
+
export function doctrineEntries(pkgRoot) {
|
|
27
|
+
const entries = [];
|
|
23
28
|
for (const kind of ['skills', 'agents', 'hooks']) {
|
|
24
29
|
const from = join(pkgRoot, kind);
|
|
25
30
|
if (!existsSync(from)) continue;
|
|
26
31
|
for (const entry of readdirSync(from)) {
|
|
27
32
|
if (kind === 'hooks' && entry === 'hooks.json') continue; // plugin-only manifest, not a script to install
|
|
28
|
-
|
|
29
|
-
const d = join(claudeDir, kind, entry);
|
|
30
|
-
rmSync(d, { recursive: true, force: true });
|
|
31
|
-
mkdirSync(dirname(d), { recursive: true });
|
|
32
|
-
cpSync(s, d, { recursive: true });
|
|
33
|
-
if (kind === 'hooks') chmodSync(d, 0o755);
|
|
34
|
-
synced.push({ kind, name: entry, dest: d });
|
|
33
|
+
entries.push({ kind, name: entry, source: join(from, entry) });
|
|
35
34
|
}
|
|
36
35
|
}
|
|
36
|
+
return entries;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {string} pkgRoot - root of the tune-context package (contains skills/, agents/, hooks/)
|
|
41
|
+
* @param {string} claudeDir - destination Claude config dir (e.g. ~/.claude)
|
|
42
|
+
* @returns {{kind: string, name: string, dest: string}[]} synced items
|
|
43
|
+
*/
|
|
44
|
+
export function syncDoctrine(pkgRoot, claudeDir) {
|
|
45
|
+
const synced = [];
|
|
46
|
+
for (const { kind, name, source } of doctrineEntries(pkgRoot)) {
|
|
47
|
+
const d = join(claudeDir, kind, name);
|
|
48
|
+
rmSync(d, { recursive: true, force: true });
|
|
49
|
+
mkdirSync(dirname(d), { recursive: true });
|
|
50
|
+
cpSync(source, d, { recursive: true });
|
|
51
|
+
if (kind === 'hooks') chmodSync(d, 0o755);
|
|
52
|
+
synced.push({ kind, name, dest: d });
|
|
53
|
+
}
|
|
37
54
|
return synced;
|
|
38
55
|
}
|
package/cli/verify.mjs
CHANGED
|
@@ -1,9 +1,43 @@
|
|
|
1
1
|
// Re-checks what generate.mjs claims to have done — "the merge actually
|
|
2
2
|
// landed" per SKILL.md §3. Never trust that a write succeeded just because
|
|
3
3
|
// the function returned; re-read the target file and parse it.
|
|
4
|
-
import {
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
5
6
|
import { homedir } from 'node:os';
|
|
6
7
|
import { basename, join, resolve } from 'node:path';
|
|
8
|
+
import { doctrineEntries } from './sync-doctrine.mjs';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Content fingerprint of a deployed artifact — a file, or a whole directory
|
|
12
|
+
* (skills are directories; `skills/tune-context` carries its own templates/).
|
|
13
|
+
*
|
|
14
|
+
* A directory hashes as the sorted list of `relative path + content hash`, so a
|
|
15
|
+
* file MISSING from inside a synced skill, or an extra one left behind by an
|
|
16
|
+
* older shape, is drift too — not just an edited byte. Sorting is what makes it
|
|
17
|
+
* reproducible across filesystems that hand back readdir in different orders.
|
|
18
|
+
*
|
|
19
|
+
* Mode is deliberately not hashed: syncDoctrine chmods hooks to 755 after
|
|
20
|
+
* copying, so folding the exec bit in here would report a rewrite as drift on
|
|
21
|
+
* every platform where the package's own bits differ. The exec bit already has
|
|
22
|
+
* its own check.
|
|
23
|
+
*
|
|
24
|
+
* @returns {string|null} null when the path does not exist
|
|
25
|
+
*/
|
|
26
|
+
export function fingerprint(path) {
|
|
27
|
+
if (!existsSync(path)) return null;
|
|
28
|
+
const hash = createHash('sha256');
|
|
29
|
+
const walk = (current, rel) => {
|
|
30
|
+
const stat = statSync(current);
|
|
31
|
+
if (stat.isDirectory()) {
|
|
32
|
+
for (const entry of readdirSync(current).sort()) walk(join(current, entry), rel ? `${rel}/${entry}` : entry);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
hash.update(`${rel}\0`);
|
|
36
|
+
hash.update(createHash('sha256').update(readFileSync(current)).digest());
|
|
37
|
+
};
|
|
38
|
+
walk(path, '');
|
|
39
|
+
return hash.digest('hex');
|
|
40
|
+
}
|
|
7
41
|
|
|
8
42
|
function readJson(path) {
|
|
9
43
|
try {
|
|
@@ -50,9 +84,83 @@ function expandHome(p) {
|
|
|
50
84
|
const PLUGIN_PATH_HINT = /\$\{CLAUDE_PLUGIN_ROOT\}|[\\/]plugins[\\/]/;
|
|
51
85
|
|
|
52
86
|
/**
|
|
53
|
-
*
|
|
87
|
+
* Are the deployed copies still the ones this package ships? (TASK-081)
|
|
88
|
+
*
|
|
89
|
+
* `npm i -g` updates the package; it does NOT touch what an earlier `init`
|
|
90
|
+
* copied into ~/.claude. settings.json points at those copies, so a release
|
|
91
|
+
* that changes hooks/ goes live only after another `init`. Observed right after
|
|
92
|
+
* publishing 2.1.0: the CLI reported 2.1.0, verify was all green, and the
|
|
93
|
+
* deployed context-reminder.mjs was still the 2.0.x file — the *only* file the
|
|
94
|
+
* release had changed.
|
|
95
|
+
*
|
|
96
|
+
* Two rules keep this from crying wolf:
|
|
97
|
+
*
|
|
98
|
+
* - **Only what is actually deployed is compared.** An artifact absent from
|
|
99
|
+
* claudeDir is not reported here at all, which is what makes a plugin-channel
|
|
100
|
+
* install (registrations pointing at `${CLAUDE_PLUGIN_ROOT}`, no copies
|
|
101
|
+
* anywhere) impossible to false-red: there is nothing to compare, so nothing
|
|
102
|
+
* is claimed. Presence of the things that MUST exist is a separate check.
|
|
103
|
+
* - **No package to compare against means "not checked", never a tick.** A
|
|
104
|
+
* caller that cannot say where the installed package lives gets `unknown`,
|
|
105
|
+
* which prints as `?` and neither passes nor fails — the TASK-070 rule that
|
|
106
|
+
* an unobservable thing must not be reported as verified.
|
|
107
|
+
*
|
|
108
|
+
* @param {string|null} pkgRoot - root of the installed package, or null/undefined
|
|
109
|
+
* @returns {{name: string, ok: boolean, detail?: string, unknown?: boolean}[]}
|
|
54
110
|
*/
|
|
55
|
-
export function
|
|
111
|
+
export function staleDoctrineEntries(claudeDir, pkgRoot) {
|
|
112
|
+
if (!pkgRoot || !existsSync(pkgRoot)) return null;
|
|
113
|
+
const stale = [];
|
|
114
|
+
let compared = 0;
|
|
115
|
+
for (const { kind, name, source } of doctrineEntries(pkgRoot)) {
|
|
116
|
+
const deployed = join(claudeDir, kind, name);
|
|
117
|
+
const deployedPrint = fingerprint(deployed);
|
|
118
|
+
if (deployedPrint === null) continue; // not deployed through this channel — not ours to judge
|
|
119
|
+
compared += 1;
|
|
120
|
+
if (deployedPrint !== fingerprint(source)) stale.push(`${kind}/${name}`);
|
|
121
|
+
}
|
|
122
|
+
return { compared, stale };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function freshnessChecks(claudeDir, pkgRoot) {
|
|
126
|
+
const result = staleDoctrineEntries(claudeDir, pkgRoot);
|
|
127
|
+
if (result === null) {
|
|
128
|
+
return [
|
|
129
|
+
{
|
|
130
|
+
name: 'deployed skills/agents/hooks match the installed package',
|
|
131
|
+
ok: false,
|
|
132
|
+
unknown: true,
|
|
133
|
+
detail: 'not checked — no installed package to compare against; re-run `tune-context init` if a release changed hooks/',
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const { compared, stale } = result;
|
|
139
|
+
if (compared === 0) {
|
|
140
|
+
return [
|
|
141
|
+
{
|
|
142
|
+
name: 'deployed skills/agents/hooks match the installed package',
|
|
143
|
+
ok: false,
|
|
144
|
+
unknown: true,
|
|
145
|
+
detail: `not checked — nothing from this package is deployed under ${claudeDir}`,
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return [
|
|
151
|
+
{
|
|
152
|
+
name: `deployed skills/agents/hooks match the installed package (${compared} compared)`,
|
|
153
|
+
ok: stale.length === 0,
|
|
154
|
+
detail: stale.length ? `stale: ${stale.join(', ')} — content differs from the installed package; re-run \`tune-context init\`` : undefined,
|
|
155
|
+
},
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @param {string} [pkgRoot] - installed package root, for the staleness check
|
|
161
|
+
* @returns {{ok: boolean, checks: {name: string, ok: boolean, detail?: string, unknown?: boolean}[]}}
|
|
162
|
+
*/
|
|
163
|
+
export function verify({ claudeDir, userSettingsPath, expectedHookCommands, projectSettingsPath, expectedPermissions, pkgRoot }) {
|
|
56
164
|
const checks = [];
|
|
57
165
|
|
|
58
166
|
// Cross-check, not two independent presence checks. The old code asked
|
|
@@ -164,5 +272,9 @@ export function verify({ claudeDir, userSettingsPath, expectedHookCommands, proj
|
|
|
164
272
|
checks.push({ name: `skill ${skill} installed`, ok, detail: ok ? undefined : `missing ${join(skillsDir, skill, 'SKILL.md')}` });
|
|
165
273
|
}
|
|
166
274
|
|
|
167
|
-
|
|
275
|
+
checks.push(...freshnessChecks(claudeDir, pkgRoot));
|
|
276
|
+
|
|
277
|
+
// `unknown` is neither a pass nor a failure: it must not turn a healthy
|
|
278
|
+
// install red, and it must not be counted as verified either.
|
|
279
|
+
return { ok: checks.every((c) => c.ok || c.unknown === true), checks };
|
|
168
280
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bglocation/tune-context",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Context-efficiency configurator for Claude Code \u2014 token-saving doctrine packaged as skills, plus a tune-context configurator that scaffolds a lean CLAUDE.md, doctrine skills, subagents, settings, hooks and MCP wiring.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,6 +22,17 @@ than rewriting the file. For JSON (`.mcp.json`, `settings.json`) there are no
|
|
|
22
22
|
markers — merge key-by-key and preserve every existing key. This is what makes a
|
|
23
23
|
second run a no-op.
|
|
24
24
|
|
|
25
|
+
**Only derived content goes inside the block.** The block carries what detection
|
|
26
|
+
produced — project name, build/test/lint, MCP rules, the doctrine pointer. The
|
|
27
|
+
sections a human writes (what this is, architecture, conventions) are seeded
|
|
28
|
+
**below** the closing marker from `CLAUDE.md.project.authored.tmpl`, once, and
|
|
29
|
+
are never regenerated. Before TASK-082 they sat inside the block behind "fill in"
|
|
30
|
+
prompts, i.e. the tool asked the user to write in the one place it overwrites;
|
|
31
|
+
re-running it replaced a repo's architecture notes with placeholders. If you meet
|
|
32
|
+
a CLAUDE.md still in that shape — authored text inside the markers — **do not
|
|
33
|
+
regenerate it**: move those sections below the closing marker first (the CLI
|
|
34
|
+
refuses outright and prints the same instruction).
|
|
35
|
+
|
|
25
36
|
**Delta only, never duplicate.** If the existing hand-written CLAUDE.md already
|
|
26
37
|
covers the template's content (build/test/lint, architecture, conventions), the
|
|
27
38
|
managed block carries **only what is missing** — usually just pointers to the
|
|
@@ -98,10 +109,13 @@ covering the detected build/test/lint commands.
|
|
|
98
109
|
|
|
99
110
|
## 2. Generate (idempotent, never clobber)
|
|
100
111
|
|
|
101
|
-
1. **Project `./CLAUDE.md`** from `${CLAUDE_SKILL_DIR}/templates/CLAUDE.md.project.tmpl
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
112
|
+
1. **Project `./CLAUDE.md`** from `${CLAUDE_SKILL_DIR}/templates/CLAUDE.md.project.tmpl`
|
|
113
|
+
(managed block) plus `CLAUDE.md.project.authored.tmpl` (the half below it).
|
|
114
|
+
Fill the block's slots from detection; delete the slot guide comment; keep the
|
|
115
|
+
managed markers. Seed the authored half **only when creating the file** — if
|
|
116
|
+
the file exists, replace the managed block and nothing else. Never write
|
|
117
|
+
detected facts below the marker, and never write authored prose above it: the
|
|
118
|
+
split is what makes re-running safe.
|
|
105
119
|
2. **User `~/.claude/CLAUDE.md`** from `${CLAUDE_SKILL_DIR}/templates/CLAUDE.md.user.tmpl`
|
|
106
120
|
— only if it is missing or clearly untuned. Never overwrite a user-tuned file
|
|
107
121
|
without asking.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!-- Everything below is yours. tune-context seeds these headings once, when it
|
|
2
|
+
first creates this file, and never rewrites them: re-running the tool
|
|
3
|
+
replaces only the managed block above. Delete whatever you don't need.
|
|
4
|
+
Keep it thin — headings and pointers here, depth in skills. -->
|
|
5
|
+
|
|
6
|
+
## What this is
|
|
7
|
+
|
|
8
|
+
<!-- fill in: one sentence, what this project is -->
|
|
9
|
+
|
|
10
|
+
## Architecture
|
|
11
|
+
|
|
12
|
+
<!-- fill in: 3-6 lines, module -> responsibility -->
|
|
13
|
+
|
|
14
|
+
## Conventions that bite
|
|
15
|
+
|
|
16
|
+
<!-- fill in: only rules that bite (things that break if ignored) -->
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<!-- >>> tune-context (managed) >>>
|
|
2
|
-
Regenerated by /tune-context
|
|
3
|
-
|
|
2
|
+
Regenerated by /tune-context and `tune-context init`. Everything between
|
|
3
|
+
these markers is DERIVED from detection and is rewritten on every run —
|
|
4
|
+
never hand-edit it. Your own content goes BELOW the block, where the tool
|
|
5
|
+
never reaches. -->
|
|
4
6
|
# CLAUDE.md — {{PROJECT_NAME}}
|
|
5
7
|
|
|
6
|
-
{{ONE_LINE_WHAT_THIS_IS}}
|
|
7
|
-
|
|
8
8
|
## Build / test / lint
|
|
9
9
|
```bash
|
|
10
10
|
{{BUILD_CMD}}
|
|
@@ -17,23 +17,14 @@
|
|
|
17
17
|
{{MCP_RULES}}
|
|
18
18
|
Depth lives in skills: `token-efficiency`, `cdd`, `phase-workflow` (and
|
|
19
19
|
`/caveman` for terse output) — not in this file.
|
|
20
|
-
|
|
21
|
-
## Architecture
|
|
22
|
-
{{ARCHITECTURE_BULLETS}}
|
|
23
|
-
|
|
24
|
-
## Conventions that bite
|
|
25
|
-
{{CONVENTIONS}}
|
|
26
|
-
|
|
27
20
|
<!-- <<< tune-context (managed) <<< -->
|
|
28
21
|
|
|
29
22
|
<!--
|
|
30
|
-
Slot guide (
|
|
23
|
+
Slot guide (managed block only — all of these are filled from detection):
|
|
31
24
|
PROJECT_NAME package/repo name
|
|
32
|
-
ONE_LINE_WHAT_THIS_IS one sentence, what the project is
|
|
33
25
|
BUILD_CMD/TEST_CMD/LINT_CMD verified commands (must actually exist)
|
|
34
26
|
RUN_NOTES how to run locally, if non-obvious
|
|
35
27
|
MCP_RULES one line per detected MCP server (see tune-context skill)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
Keep this file thin: headings + pointers. Depth goes into skills.
|
|
28
|
+
Sections you write by hand are seeded once from CLAUDE.md.project.authored.tmpl
|
|
29
|
+
and live BELOW the block. Nothing outside the markers is ever regenerated.
|
|
39
30
|
-->
|