voidforge-build 23.19.0 → 23.21.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/dist/.claude/agents/celebrimbor-forge-artist.md +1 -0
- package/dist/.claude/agents/ducem-token-economics.md +1 -0
- package/dist/.claude/agents/galadriel-frontend.md +1 -0
- package/dist/.claude/agents/romanoff-integrations.md +4 -0
- package/dist/.claude/agents/silver-surfer-herald.md +19 -4
- package/dist/.claude/commands/architect.md +4 -3
- package/dist/.claude/commands/assemble.md +12 -0
- package/dist/.claude/commands/assess.md +1 -0
- package/dist/.claude/commands/build.md +8 -0
- package/dist/.claude/commands/contextmeter.md +56 -0
- package/dist/.claude/commands/debrief.md +10 -0
- package/dist/.claude/commands/engage.md +5 -0
- package/dist/.claude/commands/git.md +19 -3
- package/dist/.claude/commands/imagine.md +1 -1
- package/dist/.claude/commands/seal.md +81 -0
- package/dist/.claude/commands/ux.md +13 -0
- package/dist/.claude/workflows/gauntlet.workflow.js +13 -1
- package/dist/CHANGELOG.md +63 -0
- package/dist/CLAUDE.md +10 -1
- package/dist/HOLOCRON.md +16 -2
- package/dist/VERSION.md +3 -1
- package/dist/docs/methods/AI_INTELLIGENCE.md +3 -0
- package/dist/docs/methods/ASSEMBLER.md +12 -0
- package/dist/docs/methods/BUILD_PROTOCOL.md +15 -0
- package/dist/docs/methods/CAMPAIGN.md +11 -0
- package/dist/docs/methods/DEVOPS_ENGINEER.md +66 -0
- package/dist/docs/methods/FIELD_MEDIC.md +1 -0
- package/dist/docs/methods/FORGE_ARTIST.md +3 -4
- package/dist/docs/methods/GAUNTLET.md +6 -0
- package/dist/docs/methods/MUSTER.md +2 -0
- package/dist/docs/methods/PRODUCT_DESIGN_FRONTEND.md +18 -0
- package/dist/docs/methods/QA_ENGINEER.md +21 -1
- package/dist/docs/methods/RELEASE_MANAGER.md +38 -0
- package/dist/docs/methods/SECURITY_AUDITOR.md +11 -1
- package/dist/docs/methods/SUB_AGENTS.md +33 -0
- package/dist/docs/methods/SYSTEMS_ARCHITECT.md +15 -0
- package/dist/docs/methods/TESTING.md +2 -0
- package/dist/docs/methods/TROUBLESHOOTING.md +2 -2
- package/dist/docs/methods/WORKFLOWS.md +14 -0
- package/dist/docs/patterns/ai-prompt-safety.ts +85 -0
- package/dist/docs/patterns/data-pipeline.ts +59 -1
- package/dist/docs/patterns/egress-sandbox.sh +43 -0
- package/dist/docs/patterns/exclusion-set-invariant.md +62 -0
- package/dist/docs/patterns/multi-tenant-property-test.ts +64 -0
- package/dist/docs/patterns/nginx-vhost.conf +156 -0
- package/dist/docs/patterns/oauth-token-lifecycle.ts +21 -0
- package/dist/docs/patterns/post-deploy-probe.sh +115 -0
- package/dist/docs/patterns/rls-test-fixture.py +140 -0
- package/dist/docs/patterns/structural-sql-sentinel.py +134 -0
- package/dist/scripts/statusline/README.md +38 -0
- package/dist/scripts/statusline/context-awareness-hook.sh +53 -0
- package/dist/scripts/statusline/settings-snippet.json +17 -0
- package/dist/scripts/statusline/voidforge-statusline.sh +91 -0
- package/dist/scripts/voidforge.js +69 -6
- package/dist/wizard/lib/claude-md-strategy.d.ts +87 -0
- package/dist/wizard/lib/claude-md-strategy.js +198 -0
- package/dist/wizard/lib/marker.d.ts +48 -1
- package/dist/wizard/lib/marker.js +58 -2
- package/dist/wizard/lib/patterns/oauth-token-lifecycle.d.ts +14 -0
- package/dist/wizard/lib/patterns/oauth-token-lifecycle.js +21 -0
- package/dist/wizard/lib/project-init.js +59 -0
- package/dist/wizard/lib/updater.d.ts +19 -0
- package/dist/wizard/lib/updater.js +84 -33
- package/package.json +2 -2
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
* The CLI walks up from cwd to find it, determining the project root.
|
|
6
6
|
*/
|
|
7
7
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
8
|
-
import { existsSync, statSync } from 'node:fs';
|
|
8
|
+
import { existsSync, statSync, readFileSync } from 'node:fs';
|
|
9
9
|
import { join, dirname, resolve } from 'node:path';
|
|
10
10
|
import { randomUUID } from 'node:crypto';
|
|
11
11
|
import { homedir } from 'node:os';
|
|
12
|
+
/** Safe default when a marker omits `claudeMd`. Never silently clobber. */
|
|
13
|
+
export const DEFAULT_CLAUDE_MD_STRATEGY = 'preserve';
|
|
12
14
|
// ── Constants ────────────────────────────────────────────
|
|
13
15
|
export const MARKER_FILE = '.voidforge';
|
|
14
16
|
// ── Read / Write ─────────────────────────────────────────
|
|
@@ -31,13 +33,14 @@ export async function writeMarker(dir, marker) {
|
|
|
31
33
|
const markerPath = join(dir, MARKER_FILE);
|
|
32
34
|
await writeFile(markerPath, JSON.stringify(marker, null, 2) + '\n', 'utf-8');
|
|
33
35
|
}
|
|
34
|
-
export function createMarker(version, tier = 'full', extensions = []) {
|
|
36
|
+
export function createMarker(version, tier = 'full', extensions = [], claudeMd = DEFAULT_CLAUDE_MD_STRATEGY) {
|
|
35
37
|
return {
|
|
36
38
|
id: randomUUID(),
|
|
37
39
|
version,
|
|
38
40
|
created: new Date().toISOString(),
|
|
39
41
|
tier,
|
|
40
42
|
extensions,
|
|
43
|
+
claudeMd,
|
|
41
44
|
};
|
|
42
45
|
}
|
|
43
46
|
// ── Project Detection ────────────────────────────────────
|
|
@@ -89,6 +92,59 @@ export function requireProjectRoot(startDir = process.cwd()) {
|
|
|
89
92
|
}
|
|
90
93
|
return root;
|
|
91
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Detect a legacy methodology consumer that predates the `.voidforge` marker.
|
|
97
|
+
*
|
|
98
|
+
* Such projects originally consumed methodology via git (before the marker
|
|
99
|
+
* convention) and so `update` hard-errors them toward `init` — which is the
|
|
100
|
+
* wrong remedy on an existing project (issue #369). The signature of a real
|
|
101
|
+
* consumer is the methodology footprint: `VERSION.md` + `.claude/commands/` +
|
|
102
|
+
* `docs/methods/` all present. When that holds and NO marker exists, the CLI
|
|
103
|
+
* should OFFER to create the marker rather than send the user to `init`.
|
|
104
|
+
*
|
|
105
|
+
* Tier is inferred from `wizard/` presence (full-tier projects embed/embedded
|
|
106
|
+
* the wizard), mirroring the workaround in the field report.
|
|
107
|
+
*
|
|
108
|
+
* Returns null when the dir already has a marker (not legacy) or does not look
|
|
109
|
+
* like a methodology consumer (genuinely not a VoidForge project).
|
|
110
|
+
*/
|
|
111
|
+
export function detectLegacyConsumer(dir = process.cwd()) {
|
|
112
|
+
const root = resolve(dir);
|
|
113
|
+
// If a valid marker is already present anywhere up the tree, it is not legacy.
|
|
114
|
+
if (findProjectRoot(root) !== null)
|
|
115
|
+
return null;
|
|
116
|
+
const hasVersion = existsSync(join(root, 'VERSION.md'));
|
|
117
|
+
const hasCommands = isDir(join(root, '.claude', 'commands'));
|
|
118
|
+
const hasMethods = isDir(join(root, 'docs', 'methods'));
|
|
119
|
+
if (!(hasVersion && hasCommands && hasMethods))
|
|
120
|
+
return null;
|
|
121
|
+
const inferredTier = isDir(join(root, 'wizard'))
|
|
122
|
+
? 'full'
|
|
123
|
+
: 'methodology';
|
|
124
|
+
return { dir: root, inferredTier };
|
|
125
|
+
}
|
|
126
|
+
/** Read the methodology version from a project's VERSION.md, or 'unknown'. */
|
|
127
|
+
export function readVersionFile(dir) {
|
|
128
|
+
const versionPath = join(resolve(dir), 'VERSION.md');
|
|
129
|
+
if (!existsSync(versionPath))
|
|
130
|
+
return 'unknown';
|
|
131
|
+
try {
|
|
132
|
+
const raw = readFileSync(versionPath, 'utf-8');
|
|
133
|
+
const match = raw.match(/(\d+\.\d+\.\d+)/);
|
|
134
|
+
return match ? match[1] : 'unknown';
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return 'unknown';
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function isDir(p) {
|
|
141
|
+
try {
|
|
142
|
+
return existsSync(p) && statSync(p).isDirectory();
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
92
148
|
// ── Global Config ────────────────────────────────────────
|
|
93
149
|
export function getGlobalDir() {
|
|
94
150
|
const home = process.env['HOME'] ?? process.env['USERPROFILE'] ?? homedir();
|
|
@@ -8,6 +8,20 @@
|
|
|
8
8
|
* - Failure escalation: retry 3x → pause platform → alert → requires_reauth
|
|
9
9
|
* - Token stored as encrypted blob in vault, keyed by platform name
|
|
10
10
|
* - Session token (daemon) rotates every 24 hours (§9.19.15)
|
|
11
|
+
* - VERIFY EXPIRY + REFRESH-GRANT BEHAVIOR AGAINST THE PROVIDER'S LIVE DOCS AT
|
|
12
|
+
* INTEGRATION TIME. The PLATFORM_CONFIGS TTLs below are STARTING ASSUMPTIONS,
|
|
13
|
+
* not ground truth — providers change them and "no refresh token / never
|
|
14
|
+
* expires" is a common false assumption. Field report #373: a Todoist
|
|
15
|
+
* integration shipped on "tokens don't expire," but the modern API issues
|
|
16
|
+
* ~1h access tokens WITH a refresh token; the code discarded the refresh
|
|
17
|
+
* token + expiry and registered no refresher, so it died ~1h after every
|
|
18
|
+
* connect across four sessions — looking exactly like intermittent
|
|
19
|
+
* revocation. At integration time: (1) read the provider's OAuth docs and
|
|
20
|
+
* quote the verified access-token TTL + whether a refresh_token is issued;
|
|
21
|
+
* (2) if a refresh_token exists, PERSIST it and register a refresher — never
|
|
22
|
+
* discard it; (3) distinguish "expired" from "revoked" via the API's OWN
|
|
23
|
+
* error body, not by inference (an expired token that mimics revocation will
|
|
24
|
+
* send you reauth-hunting instead of refreshing).
|
|
11
25
|
*
|
|
12
26
|
* Agents: Breeze (platform relations), Dockson (vault)
|
|
13
27
|
*
|
|
@@ -8,11 +8,32 @@
|
|
|
8
8
|
* - Failure escalation: retry 3x → pause platform → alert → requires_reauth
|
|
9
9
|
* - Token stored as encrypted blob in vault, keyed by platform name
|
|
10
10
|
* - Session token (daemon) rotates every 24 hours (§9.19.15)
|
|
11
|
+
* - VERIFY EXPIRY + REFRESH-GRANT BEHAVIOR AGAINST THE PROVIDER'S LIVE DOCS AT
|
|
12
|
+
* INTEGRATION TIME. The PLATFORM_CONFIGS TTLs below are STARTING ASSUMPTIONS,
|
|
13
|
+
* not ground truth — providers change them and "no refresh token / never
|
|
14
|
+
* expires" is a common false assumption. Field report #373: a Todoist
|
|
15
|
+
* integration shipped on "tokens don't expire," but the modern API issues
|
|
16
|
+
* ~1h access tokens WITH a refresh token; the code discarded the refresh
|
|
17
|
+
* token + expiry and registered no refresher, so it died ~1h after every
|
|
18
|
+
* connect across four sessions — looking exactly like intermittent
|
|
19
|
+
* revocation. At integration time: (1) read the provider's OAuth docs and
|
|
20
|
+
* quote the verified access-token TTL + whether a refresh_token is issued;
|
|
21
|
+
* (2) if a refresh_token exists, PERSIST it and register a refresher — never
|
|
22
|
+
* discard it; (3) distinguish "expired" from "revoked" via the API's OWN
|
|
23
|
+
* error body, not by inference (an expired token that mimics revocation will
|
|
24
|
+
* send you reauth-hunting instead of refreshing).
|
|
11
25
|
*
|
|
12
26
|
* Agents: Breeze (platform relations), Dockson (vault)
|
|
13
27
|
*
|
|
14
28
|
* PRD Reference: §9.5 (token refresh strategy), §9.18 (vault session), §9.19.15
|
|
15
29
|
*/
|
|
30
|
+
// ASSUMPTIONS, NOT GROUND TRUTH (field report #373). These TTLs and the
|
|
31
|
+
// "refreshTokenTtlDays: 0 = never expires" entries are starting points. At
|
|
32
|
+
// integration time, VERIFY each value against the provider's current OAuth
|
|
33
|
+
// docs and the live token response (`expires_in`, presence of `refresh_token`)
|
|
34
|
+
// — a provider that "doesn't expire" today may issue ~1h tokens tomorrow, and
|
|
35
|
+
// a missing refresher then surfaces as recurring prod token-death that mimics
|
|
36
|
+
// revocation. Treat any new platform here the same way before shipping.
|
|
16
37
|
const PLATFORM_CONFIGS = [
|
|
17
38
|
{ platform: 'meta', accessTokenTtlHours: 1440, refreshTokenTtlDays: 0, refreshEndpoint: 'https://graph.facebook.com/v19.0/oauth/access_token' },
|
|
18
39
|
{ platform: 'google', accessTokenTtlHours: 1, refreshTokenTtlDays: 0, refreshEndpoint: 'https://oauth2.googleapis.com/token' },
|
|
@@ -141,6 +141,16 @@ async function copyMethodology(methodologyRoot, projectDir, core) {
|
|
|
141
141
|
await chmodShellScripts(join(projectDir, 'scripts', 'surfer-gate'));
|
|
142
142
|
await mergeSettingsHook(projectDir);
|
|
143
143
|
}
|
|
144
|
+
// Context-meter status line (/contextmeter) — default-on. Ship the scripts AND wire
|
|
145
|
+
// the statusLine + UserPromptSubmit awareness hook into settings.json, the same way the
|
|
146
|
+
// surfer-gate hook is wired. Defaults: warn 80% / crit 92% (baked into the scripts).
|
|
147
|
+
// Opt out per project with `/contextmeter --uninstall`.
|
|
148
|
+
const statuslineSrc = join(methodologyRoot, 'scripts', 'statusline');
|
|
149
|
+
if (existsSync(statuslineSrc)) {
|
|
150
|
+
count += await copyDir(statuslineSrc, join(projectDir, 'scripts', 'statusline'));
|
|
151
|
+
await chmodShellScripts(join(projectDir, 'scripts', 'statusline'));
|
|
152
|
+
await mergeStatuslineSettings(projectDir);
|
|
153
|
+
}
|
|
144
154
|
return count;
|
|
145
155
|
}
|
|
146
156
|
async function chmodShellScripts(dir) {
|
|
@@ -190,6 +200,55 @@ async function mergeSettingsHook(projectDir) {
|
|
|
190
200
|
};
|
|
191
201
|
await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
192
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Wire the /contextmeter status line + awareness hook into settings.json (default-on).
|
|
205
|
+
* Mirrors mergeSettingsHook: set `statusLine` only when the project doesn't already have
|
|
206
|
+
* one (never clobber a user's), and append the UserPromptSubmit awareness hook unless an
|
|
207
|
+
* equivalent is already present (idempotent). Defaults (warn 80 / crit 92) live in the
|
|
208
|
+
* scripts, so the wired commands carry no env prefix.
|
|
209
|
+
*/
|
|
210
|
+
async function mergeStatuslineSettings(projectDir) {
|
|
211
|
+
const snippetPath = join(projectDir, 'scripts', 'statusline', 'settings-snippet.json');
|
|
212
|
+
const settingsPath = join(projectDir, '.claude', 'settings.json');
|
|
213
|
+
if (!existsSync(snippetPath))
|
|
214
|
+
return;
|
|
215
|
+
const snippet = JSON.parse(await readFile(snippetPath, 'utf-8'));
|
|
216
|
+
const snippetStatusLine = snippet?.statusLine;
|
|
217
|
+
const snippetUserPrompt = (snippet?.hooks?.UserPromptSubmit ?? []);
|
|
218
|
+
if (!snippetStatusLine && snippetUserPrompt.length === 0)
|
|
219
|
+
return;
|
|
220
|
+
let settings = {};
|
|
221
|
+
if (existsSync(settingsPath)) {
|
|
222
|
+
try {
|
|
223
|
+
settings = JSON.parse(await readFile(settingsPath, 'utf-8'));
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// Existing settings.json is unreadable — leave it alone.
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
await mkdir(join(projectDir, '.claude'), { recursive: true });
|
|
232
|
+
}
|
|
233
|
+
// statusLine: never clobber a project's existing one.
|
|
234
|
+
if (snippetStatusLine && !settings.statusLine) {
|
|
235
|
+
settings.statusLine = snippetStatusLine;
|
|
236
|
+
}
|
|
237
|
+
// UserPromptSubmit: append the awareness hook unless an equivalent is already present.
|
|
238
|
+
const existingHooks = (settings.hooks ?? {});
|
|
239
|
+
const existingUserPrompt = (existingHooks.UserPromptSubmit ?? []);
|
|
240
|
+
const alreadyHasMeter = existingUserPrompt.some((entry) => {
|
|
241
|
+
const hooks = (entry?.hooks ?? []);
|
|
242
|
+
return hooks.some((h) => typeof h?.command === 'string' && h.command.includes('context-awareness-hook'));
|
|
243
|
+
});
|
|
244
|
+
if (!alreadyHasMeter && snippetUserPrompt.length > 0) {
|
|
245
|
+
settings.hooks = {
|
|
246
|
+
...existingHooks,
|
|
247
|
+
UserPromptSubmit: [...existingUserPrompt, ...snippetUserPrompt],
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
251
|
+
}
|
|
193
252
|
// ── Identity Injection ───────────────────────────────────
|
|
194
253
|
async function injectIdentity(projectDir, config) {
|
|
195
254
|
const claudePath = join(projectDir, 'CLAUDE.md');
|
|
@@ -2,17 +2,36 @@
|
|
|
2
2
|
* Update mechanisms — methodology update (replaces /void git-fetch),
|
|
3
3
|
* self-update, and extension update.
|
|
4
4
|
*/
|
|
5
|
+
import type { ClaudeMdAction } from './claude-md-strategy.js';
|
|
5
6
|
export interface UpdatePlan {
|
|
6
7
|
added: string[];
|
|
7
8
|
modified: string[];
|
|
8
9
|
removed: string[];
|
|
9
10
|
unchanged: number;
|
|
11
|
+
/** Non-destructive CLAUDE.md handling (issue #368). */
|
|
12
|
+
claudeMd?: {
|
|
13
|
+
action: ClaudeMdAction;
|
|
14
|
+
droppedSections: string[];
|
|
15
|
+
warnings: string[];
|
|
16
|
+
/** Side file written instead of CLAUDE.md, relative to project root. */
|
|
17
|
+
sideFile?: string;
|
|
18
|
+
};
|
|
10
19
|
}
|
|
11
20
|
export interface UpdateResult {
|
|
12
21
|
applied: boolean;
|
|
13
22
|
plan: UpdatePlan;
|
|
14
23
|
newVersion: string;
|
|
15
24
|
}
|
|
25
|
+
export type UpdateMode = 'help' | 'self' | 'extensions' | 'methodology';
|
|
26
|
+
/**
|
|
27
|
+
* Decide which `update` mode the given argv selects. Pure — no I/O, no exit.
|
|
28
|
+
*
|
|
29
|
+
* Help MUST win over every action flag (issue #368): `update --help` printed
|
|
30
|
+
* usage but the OLD router fell through and EXECUTED the (destructive) update.
|
|
31
|
+
* Centralizing the precedence here makes that ordering testable and keeps the
|
|
32
|
+
* CLI from re-introducing the bug.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveUpdateMode(args: string[]): UpdateMode;
|
|
16
35
|
/**
|
|
17
36
|
* Diff methodology source against project files.
|
|
18
37
|
* Returns a plan showing what would change.
|
|
@@ -2,11 +2,29 @@
|
|
|
2
2
|
* Update mechanisms — methodology update (replaces /void git-fetch),
|
|
3
3
|
* self-update, and extension update.
|
|
4
4
|
*/
|
|
5
|
-
import { readFile, readdir, cp } from 'node:fs/promises';
|
|
5
|
+
import { readFile, readdir, cp, writeFile } from 'node:fs/promises';
|
|
6
6
|
import { existsSync } from 'node:fs';
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { execSync } from 'node:child_process';
|
|
9
|
-
import { readMarker, writeMarker } from './marker.js';
|
|
9
|
+
import { readMarker, writeMarker, DEFAULT_CLAUDE_MD_STRATEGY } from './marker.js';
|
|
10
|
+
import { planClaudeMdUpdate, UPSTREAM_SUFFIX } from './claude-md-strategy.js';
|
|
11
|
+
/**
|
|
12
|
+
* Decide which `update` mode the given argv selects. Pure — no I/O, no exit.
|
|
13
|
+
*
|
|
14
|
+
* Help MUST win over every action flag (issue #368): `update --help` printed
|
|
15
|
+
* usage but the OLD router fell through and EXECUTED the (destructive) update.
|
|
16
|
+
* Centralizing the precedence here makes that ordering testable and keeps the
|
|
17
|
+
* CLI from re-introducing the bug.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveUpdateMode(args) {
|
|
20
|
+
if (args.includes('--help') || args.includes('-h'))
|
|
21
|
+
return 'help';
|
|
22
|
+
if (args.includes('--self'))
|
|
23
|
+
return 'self';
|
|
24
|
+
if (args.includes('--extensions'))
|
|
25
|
+
return 'extensions';
|
|
26
|
+
return 'methodology';
|
|
27
|
+
}
|
|
10
28
|
// ── Methodology Source Resolution ────────────────────────
|
|
11
29
|
async function resolveMethodologySource() {
|
|
12
30
|
const dir = import.meta.dirname ?? new URL('.', import.meta.url).pathname;
|
|
@@ -47,6 +65,23 @@ async function collectFiles(dir, base = '') {
|
|
|
47
65
|
}
|
|
48
66
|
return files;
|
|
49
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Compute the non-destructive CLAUDE.md plan for a project (issue #368).
|
|
70
|
+
* Reads the marker's `claudeMd` strategy (default 'preserve') and delegates the
|
|
71
|
+
* decision to the pure planner. Returns null only when there is no upstream
|
|
72
|
+
* CLAUDE.md to apply.
|
|
73
|
+
*/
|
|
74
|
+
async function planClaudeMd(sourceRoot, projectDir) {
|
|
75
|
+
const srcPath = join(sourceRoot, 'CLAUDE.md');
|
|
76
|
+
if (!existsSync(srcPath))
|
|
77
|
+
return null;
|
|
78
|
+
const upstream = await readFile(srcPath, 'utf-8');
|
|
79
|
+
const destPath = join(projectDir, 'CLAUDE.md');
|
|
80
|
+
const current = existsSync(destPath) ? await readFile(destPath, 'utf-8') : null;
|
|
81
|
+
const marker = await readMarker(projectDir);
|
|
82
|
+
const strategy = marker?.claudeMd ?? DEFAULT_CLAUDE_MD_STRATEGY;
|
|
83
|
+
return planClaudeMdUpdate(current, upstream, strategy);
|
|
84
|
+
}
|
|
50
85
|
/**
|
|
51
86
|
* Diff methodology source against project files.
|
|
52
87
|
* Returns a plan showing what would change.
|
|
@@ -68,9 +103,33 @@ export async function diffMethodology(projectDir) {
|
|
|
68
103
|
{ src: 'docs/patterns', dest: 'docs/patterns' },
|
|
69
104
|
{ src: 'scripts/thumper', dest: 'scripts/thumper' },
|
|
70
105
|
{ src: 'scripts/surfer-gate', dest: 'scripts/surfer-gate' },
|
|
106
|
+
// Context-meter status line + awareness hook (/contextmeter). Scripts propagate on
|
|
107
|
+
// update; activation (statusLine + UserPromptSubmit hook in settings.json) stays opt-in.
|
|
108
|
+
{ src: 'scripts/statusline', dest: 'scripts/statusline' },
|
|
71
109
|
];
|
|
72
|
-
//
|
|
73
|
-
|
|
110
|
+
// CLAUDE.md is handled via the non-destructive strategy mechanism (issue #368)
|
|
111
|
+
// — never the old "preserve first 10 lines, overwrite the rest" clobber.
|
|
112
|
+
const claudeMdPlan = await planClaudeMd(sourceRoot, projectDir);
|
|
113
|
+
if (claudeMdPlan) {
|
|
114
|
+
plan.claudeMd = {
|
|
115
|
+
action: claudeMdPlan.action,
|
|
116
|
+
droppedSections: claudeMdPlan.droppedSections,
|
|
117
|
+
warnings: claudeMdPlan.warnings,
|
|
118
|
+
sideFile: claudeMdPlan.sideFileContent !== null ? `CLAUDE.md${UPSTREAM_SUFFIX}` : undefined,
|
|
119
|
+
};
|
|
120
|
+
if (claudeMdPlan.action === 'unchanged' || claudeMdPlan.action === 'skip') {
|
|
121
|
+
plan.unchanged++;
|
|
122
|
+
}
|
|
123
|
+
else if (claudeMdPlan.action === 'side-file') {
|
|
124
|
+
// The side file is the only thing that changes; CLAUDE.md itself is untouched.
|
|
125
|
+
plan.modified.push(`CLAUDE.md${UPSTREAM_SUFFIX}`);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
plan.modified.push('CLAUDE.md');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Other single files compare/copy verbatim (no special preservation needed).
|
|
132
|
+
const singleFiles = ['HOLOCRON.md', 'VERSION.md'];
|
|
74
133
|
// Check single files
|
|
75
134
|
for (const file of singleFiles) {
|
|
76
135
|
const srcPath = join(sourceRoot, file);
|
|
@@ -83,21 +142,10 @@ export async function diffMethodology(projectDir) {
|
|
|
83
142
|
else {
|
|
84
143
|
const srcContent = await readFile(srcPath, 'utf-8');
|
|
85
144
|
const destContent = await readFile(destPath, 'utf-8');
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (srcLines !== destLines)
|
|
91
|
-
plan.modified.push(file);
|
|
92
|
-
else
|
|
93
|
-
plan.unchanged++;
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
if (srcContent !== destContent)
|
|
97
|
-
plan.modified.push(file);
|
|
98
|
-
else
|
|
99
|
-
plan.unchanged++;
|
|
100
|
-
}
|
|
145
|
+
if (srcContent !== destContent)
|
|
146
|
+
plan.modified.push(file);
|
|
147
|
+
else
|
|
148
|
+
plan.unchanged++;
|
|
101
149
|
}
|
|
102
150
|
}
|
|
103
151
|
// Check directories
|
|
@@ -153,24 +201,27 @@ export async function applyUpdate(projectDir) {
|
|
|
153
201
|
if (plan.added.length === 0 && plan.modified.length === 0) {
|
|
154
202
|
return { applied: false, plan, newVersion };
|
|
155
203
|
}
|
|
204
|
+
// CLAUDE.md: apply the non-destructive strategy decision (issue #368).
|
|
205
|
+
// Never overwrite a customized CLAUDE.md in place. `preserve` writes a side
|
|
206
|
+
// file; `merge` replaces only the fenced block; `skip` does nothing.
|
|
207
|
+
const claudeMdPlan = await planClaudeMd(sourceRoot, projectDir);
|
|
208
|
+
const claudeMdDestPath = join(projectDir, 'CLAUDE.md');
|
|
209
|
+
if (claudeMdPlan) {
|
|
210
|
+
if (claudeMdPlan.claudeMdContent !== null) {
|
|
211
|
+
await writeFile(claudeMdDestPath, claudeMdPlan.claudeMdContent, 'utf-8');
|
|
212
|
+
}
|
|
213
|
+
if (claudeMdPlan.sideFileContent !== null) {
|
|
214
|
+
await writeFile(`${claudeMdDestPath}${UPSTREAM_SUFFIX}`, claudeMdPlan.sideFileContent, 'utf-8');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// The CLAUDE.md plan entries are handled above — exclude them from the
|
|
218
|
+
// generic verbatim copy loop so we don't double-write or clobber.
|
|
219
|
+
const claudeMdEntries = new Set(['CLAUDE.md', `CLAUDE.md${UPSTREAM_SUFFIX}`]);
|
|
156
220
|
// Copy added + modified files
|
|
157
221
|
const { mkdir } = await import('node:fs/promises');
|
|
158
222
|
for (const file of [...plan.added, ...plan.modified]) {
|
|
159
|
-
|
|
160
|
-
if (file === 'CLAUDE.md') {
|
|
161
|
-
const srcContent = await readFile(join(sourceRoot, file), 'utf-8');
|
|
162
|
-
const destPath = join(projectDir, file);
|
|
163
|
-
if (existsSync(destPath)) {
|
|
164
|
-
const destContent = await readFile(destPath, 'utf-8');
|
|
165
|
-
const destIdentity = destContent.split('\n').slice(0, 10).join('\n');
|
|
166
|
-
const srcBody = srcContent.split('\n').slice(10).join('\n');
|
|
167
|
-
await import('node:fs/promises').then(fs => fs.writeFile(destPath, destIdentity + '\n' + srcBody, 'utf-8'));
|
|
168
|
-
}
|
|
169
|
-
else {
|
|
170
|
-
await cp(join(sourceRoot, file), destPath);
|
|
171
|
-
}
|
|
223
|
+
if (claudeMdEntries.has(file))
|
|
172
224
|
continue;
|
|
173
|
-
}
|
|
174
225
|
const srcPath = join(sourceRoot, file);
|
|
175
226
|
const destPath = join(projectDir, file);
|
|
176
227
|
const destDir = join(destPath, '..');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "voidforge-build",
|
|
3
|
-
"version": "23.
|
|
3
|
+
"version": "23.21.0",
|
|
4
4
|
"description": "From nothing, everything. A methodology framework for building with Claude Code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"@aws-sdk/client-rds": "^3.700.0",
|
|
46
46
|
"@aws-sdk/client-s3": "^3.700.0",
|
|
47
47
|
"@aws-sdk/client-sts": "^3.700.0",
|
|
48
|
-
"voidforge-build-methodology": "^23.
|
|
48
|
+
"voidforge-build-methodology": "^23.21.0",
|
|
49
49
|
"node-pty": "^1.2.0-beta.12",
|
|
50
50
|
"ws": "^8.19.0"
|
|
51
51
|
},
|