create-claude-cabinet 0.27.1 → 0.27.2
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/README.md +2 -2
- package/lib/cli.js +31 -1
- package/lib/settings-merge.js +9 -0
- package/package.json +3 -2
- package/templates/scripts/project-context.cjs +103 -0
- package/templates/scripts/validate-memory.mjs +207 -0
- package/templates/scripts/write-memory-file.mjs +185 -0
- package/templates/skills/cc-publish/SKILL.md +4 -1
- package/templates/skills/cc-upgrade/phases/omega-migration-detect.md +29 -0
package/README.md
CHANGED
|
@@ -186,7 +186,7 @@ absent to use the default. No config files, no YAML, no DSL.
|
|
|
186
186
|
|
|
187
187
|
## Adding Modules to an Existing Install
|
|
188
188
|
|
|
189
|
-
Some modules (like `verify`
|
|
189
|
+
Some modules (like `verify`) are opt-in. To add one
|
|
190
190
|
without touching anything else in your install:
|
|
191
191
|
|
|
192
192
|
```
|
|
@@ -196,7 +196,7 @@ npx create-claude-cabinet --modules verify --yes
|
|
|
196
196
|
The `--modules` flag **merges** with your existing install — it adds
|
|
197
197
|
the listed modules to what's already there, it doesn't replace your
|
|
198
198
|
module set. Safe to run on a mature project without losing
|
|
199
|
-
customization. You can pass multiple modules: `--modules verify,
|
|
199
|
+
customization. You can pass multiple modules: `--modules verify,audit`.
|
|
200
200
|
|
|
201
201
|
## CLI Options
|
|
202
202
|
|
package/lib/cli.js
CHANGED
|
@@ -343,6 +343,15 @@ function generateSkillIndex(projectDir) {
|
|
|
343
343
|
return entries.length;
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
+
// MODULES is the manifest: every template path here is copied into a
|
|
347
|
+
// consumer's project on install. A skill/hook/script that exists under
|
|
348
|
+
// templates/ but is NOT listed in any module never ships — that is the
|
|
349
|
+
// orphan bug that left the whole built-in-memory layer uninstalled
|
|
350
|
+
// before v0.27.2 (test/manifest-integrity guards against recurrence).
|
|
351
|
+
//
|
|
352
|
+
// Intentional orphans (maintainer-only, must NOT ship to consumers) are
|
|
353
|
+
// allowlisted in test/manifest-integrity. Currently: skills/cc-publish
|
|
354
|
+
// (CC-source-repo release tooling, like scripts/migrate-all-consumers.js).
|
|
346
355
|
const MODULES = {
|
|
347
356
|
'session-loop': {
|
|
348
357
|
name: 'Session Loop (orient + debrief)',
|
|
@@ -367,7 +376,7 @@ const MODULES = {
|
|
|
367
376
|
mandatory: false,
|
|
368
377
|
default: true,
|
|
369
378
|
lean: true,
|
|
370
|
-
templates: ['hooks/git-guardrails.sh', 'hooks/cc-upstream-guard.sh', 'hooks/skill-telemetry.sh', 'hooks/skill-tool-telemetry.sh', 'hooks/work-tracker-guard.sh', 'hooks/action-quality-gate.sh', 'hooks/action-completion-gate.sh', 'hooks/
|
|
379
|
+
templates: ['hooks/git-guardrails.sh', 'hooks/cc-upstream-guard.sh', 'hooks/skill-telemetry.sh', 'hooks/skill-tool-telemetry.sh', 'hooks/work-tracker-guard.sh', 'hooks/action-quality-gate.sh', 'hooks/action-completion-gate.sh', 'hooks/memory-index-guard.sh', 'scripts/cc-drift-check.cjs'],
|
|
371
380
|
},
|
|
372
381
|
'work-tracking': {
|
|
373
382
|
name: 'Work Tracking (pib-db or markdown)',
|
|
@@ -394,6 +403,27 @@ const MODULES = {
|
|
|
394
403
|
lean: false,
|
|
395
404
|
templates: ['rules/enforcement-pipeline.md', 'memory/patterns/_pattern-template.md', 'memory/patterns/pattern-intelligence-first.md'],
|
|
396
405
|
},
|
|
406
|
+
'memory': {
|
|
407
|
+
name: 'Built-In Memory (cc-remember + reader + validator)',
|
|
408
|
+
description: 'Curated write/validate layer over Claude Code\'s built-in file memory. /cc-remember writes indexed memories, /memory browses them, validate-memory.mjs guards MEMORY.md integrity. Replaced the retired omega engine in v0.27.',
|
|
409
|
+
mandatory: false,
|
|
410
|
+
default: true,
|
|
411
|
+
// lean:true is load-bearing — it keeps the skill triad, scripts, and
|
|
412
|
+
// rule installing together as an atomic unit. compliance is lean:false,
|
|
413
|
+
// so the memory-capture rule must NOT live there or lean installs get a
|
|
414
|
+
// partial, incoherent set.
|
|
415
|
+
lean: true,
|
|
416
|
+
templates: [
|
|
417
|
+
'skills/cc-remember',
|
|
418
|
+
'skills/memory',
|
|
419
|
+
'rules/memory-capture.md',
|
|
420
|
+
'scripts/write-memory-file.mjs',
|
|
421
|
+
'scripts/validate-memory.mjs',
|
|
422
|
+
// project-context.cjs ships as a co-located sibling: the two .mjs
|
|
423
|
+
// scripts require('./project-context.cjs') and consumers have no lib/.
|
|
424
|
+
'scripts/project-context.cjs',
|
|
425
|
+
],
|
|
426
|
+
},
|
|
397
427
|
'audit': {
|
|
398
428
|
name: 'Audit Loop (audit + triage + cabinet)',
|
|
399
429
|
description: '27 expert cabinet members review your project. Convene the full cabinet or just one committee.',
|
package/lib/settings-merge.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-claude-cabinet",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.2",
|
|
4
4
|
"description": "Claude Cabinet — opinionated process scaffolding for Claude Code projects",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-claude-cabinet": "bin/create-claude-cabinet.js"
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"node": ">=18"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
|
-
"test": "node --test test/**/*.test.js"
|
|
27
|
+
"test": "node --test test/**/*.test.js",
|
|
28
|
+
"prepublishOnly": "node -e \"if (!process.env.CC_ALLOW_PUBLISH) { console.error('\\n\\u2717 Use /cc-publish (which runs the mandatory consumer-walk), not raw npm publish.\\n Raw npm publish on v0.27.1 left every consumer stale on v0.26.\\n To publish manually anyway, set CC_ALLOW_PUBLISH=1.\\n'); process.exit(1); }\""
|
|
28
29
|
},
|
|
29
30
|
"dependencies": {
|
|
30
31
|
"better-sqlite3": "^12.8.0",
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared project-identity and memory-dir resolution for CC's
|
|
3
|
+
* memory-related tooling. Used by:
|
|
4
|
+
* - lib/migrate-from-omega.js (Phase 1 migration)
|
|
5
|
+
* - scripts/write-memory-file.mjs (Phase 3a /cc-remember writer)
|
|
6
|
+
* - scripts/validate-memory.mjs (Phase 3a validator)
|
|
7
|
+
*
|
|
8
|
+
* Worktree-safe: resolves project identity via `git rev-parse
|
|
9
|
+
* --git-common-dir`, so callers from a worktree write to the host
|
|
10
|
+
* project's memory dir, not a per-worktree split.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('node:fs');
|
|
16
|
+
const path = require('node:path');
|
|
17
|
+
const os = require('node:os');
|
|
18
|
+
const { execSync } = require('node:child_process');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the absolute path of the current project's primary checkout.
|
|
22
|
+
* Worktree-safe via git common-dir; falls back to cwd if not in a repo.
|
|
23
|
+
*/
|
|
24
|
+
function resolveProjectAbsolutePath(cwd = process.cwd()) {
|
|
25
|
+
try {
|
|
26
|
+
const commonDir = execSync('git rev-parse --git-common-dir', {
|
|
27
|
+
cwd,
|
|
28
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
29
|
+
encoding: 'utf8',
|
|
30
|
+
}).trim();
|
|
31
|
+
const abs = path.isAbsolute(commonDir) ? commonDir : path.resolve(cwd, commonDir);
|
|
32
|
+
return path.dirname(abs);
|
|
33
|
+
} catch {
|
|
34
|
+
return path.resolve(cwd);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Project slug — the basename of the project root. Used as a
|
|
40
|
+
* human-readable identifier and to match memory keys.
|
|
41
|
+
*/
|
|
42
|
+
function resolveProjectSlug(cwd = process.cwd()) {
|
|
43
|
+
return path.basename(resolveProjectAbsolutePath(cwd));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Convert an absolute path to Claude Code's project-dir slug format
|
|
48
|
+
* (dashified). `/Users/x/claude-cabinet` → `-Users-x-claude-cabinet`.
|
|
49
|
+
*/
|
|
50
|
+
function dashifiedSlug(absolutePath) {
|
|
51
|
+
return absolutePath.replace(/^\//, '-').replace(/\//g, '-');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Read settings.json safely; returns {} on absence or parse error.
|
|
56
|
+
*/
|
|
57
|
+
function readSettings(settingsPath) {
|
|
58
|
+
if (!fs.existsSync(settingsPath)) return {};
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
61
|
+
} catch {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the memory directory for the current project.
|
|
68
|
+
* Honors `autoMemoryDirectory` setting if present in ~/.claude/settings.json.
|
|
69
|
+
* Otherwise uses platform default: ~/.claude/projects/<dashified>/memory/.
|
|
70
|
+
*
|
|
71
|
+
* Rejects literal `~` in autoMemoryDirectory — Claude Code does NOT
|
|
72
|
+
* expand tilde in settings.json values, and a silent `./~/...` directory
|
|
73
|
+
* is a footgun.
|
|
74
|
+
*/
|
|
75
|
+
function resolveMemoryDir(opts = {}) {
|
|
76
|
+
const homeDir = opts.homeDir || os.homedir();
|
|
77
|
+
const cwd = opts.cwd || process.cwd();
|
|
78
|
+
const settingsPath = opts.settingsPath || path.join(homeDir, '.claude', 'settings.json');
|
|
79
|
+
|
|
80
|
+
if (opts.memoryDir) return path.resolve(opts.memoryDir);
|
|
81
|
+
|
|
82
|
+
const settings = readSettings(settingsPath);
|
|
83
|
+
if (settings.autoMemoryDirectory) {
|
|
84
|
+
const v = settings.autoMemoryDirectory;
|
|
85
|
+
if (v.includes('~')) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`autoMemoryDirectory in ${settingsPath} contains '~' (literal). Claude Code does not expand tilde. Use an absolute path.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return path.resolve(v);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const projectAbs = resolveProjectAbsolutePath(cwd);
|
|
94
|
+
return path.join(homeDir, '.claude', 'projects', dashifiedSlug(projectAbs), 'memory');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = {
|
|
98
|
+
resolveProjectAbsolutePath,
|
|
99
|
+
resolveProjectSlug,
|
|
100
|
+
resolveMemoryDir,
|
|
101
|
+
dashifiedSlug,
|
|
102
|
+
readSettings,
|
|
103
|
+
};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate the structural integrity of a Claude Code memory directory.
|
|
3
|
+
*
|
|
4
|
+
* Checks:
|
|
5
|
+
* 1. MEMORY.md exists and is within Claude Code's session-start
|
|
6
|
+
* load budget: ≤200 lines AND ≤25KB.
|
|
7
|
+
* 2. Every .md file in the memory dir (except MEMORY.md, edges.json)
|
|
8
|
+
* is referenced by MEMORY.md (bidirectional: no orphans).
|
|
9
|
+
* 3. Every file referenced in MEMORY.md exists on disk.
|
|
10
|
+
* 4. Topic-style files (migrated from omega) do not exceed 50KB.
|
|
11
|
+
* Per-file curated style: no size cap.
|
|
12
|
+
*
|
|
13
|
+
* Wired into /validate via templates/skills/validate/phases/validators.md.
|
|
14
|
+
* Also runs PostToolUse on memory-dir writes via
|
|
15
|
+
* templates/.claude/hooks/memory-index-guard.sh.
|
|
16
|
+
*
|
|
17
|
+
* Exit codes:
|
|
18
|
+
* 0 — pass
|
|
19
|
+
* 1 — one or more violations
|
|
20
|
+
* 2 — bad usage / inaccessible memory dir
|
|
21
|
+
*
|
|
22
|
+
* @module scripts/validate-memory
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import fs from 'node:fs';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import { createRequire } from 'node:module';
|
|
28
|
+
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
const { resolveMemoryDir } = require('./project-context.cjs');
|
|
31
|
+
|
|
32
|
+
const MEMORY_INDEX_FILE = 'MEMORY.md';
|
|
33
|
+
const MEMORY_INDEX_LINE_CAP = 200;
|
|
34
|
+
const MEMORY_INDEX_BYTE_CAP = 25_000;
|
|
35
|
+
const TOPIC_FILE_SIZE_CAP = 50_000;
|
|
36
|
+
const NON_MEMORY_FILES = new Set(['MEMORY.md', 'edges.json', '.DS_Store']);
|
|
37
|
+
|
|
38
|
+
// Files that match this pattern are "topic-style" (multi-memory files
|
|
39
|
+
// produced by Phase 1 migration). The 50KB cap applies to them.
|
|
40
|
+
// Per-file curated style files (one memory each) are not size-capped.
|
|
41
|
+
const TOPIC_FILE_NAMES = new Set([
|
|
42
|
+
'decisions.md',
|
|
43
|
+
'lessons.md',
|
|
44
|
+
'preferences.md',
|
|
45
|
+
'constraints.md',
|
|
46
|
+
'session-summaries.md',
|
|
47
|
+
'subagent-residue.md',
|
|
48
|
+
'unscoped.md',
|
|
49
|
+
]);
|
|
50
|
+
const TOPIC_FILE_PATTERNS = [
|
|
51
|
+
/^(decisions|lessons|preferences|constraints|session-summaries|subagent-residue|unscoped)(-recent|-archive)?(-\d+)?\.md$/,
|
|
52
|
+
/^cross-[a-z0-9_-]+(-recent|-archive)?(-\d+)?\.md$/,
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
function isTopicStyleFile(name) {
|
|
56
|
+
if (TOPIC_FILE_NAMES.has(name)) return true;
|
|
57
|
+
return TOPIC_FILE_PATTERNS.some((re) => re.test(name));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Extract all `.md` filenames referenced from MEMORY.md.
|
|
62
|
+
* Handles both index formats:
|
|
63
|
+
* - Topic-files: `- **decisions.md** (56 entries) — desc`
|
|
64
|
+
* - Curated: `- [Title](file.md) — desc`
|
|
65
|
+
*/
|
|
66
|
+
function parseMemoryIndex(memoryMdText) {
|
|
67
|
+
const refs = new Set();
|
|
68
|
+
// Topic-files format: bolded filename
|
|
69
|
+
for (const m of memoryMdText.matchAll(/\*\*([a-z0-9_.-]+\.md)\*\*/gi)) {
|
|
70
|
+
refs.add(m[1]);
|
|
71
|
+
}
|
|
72
|
+
// Curated format: markdown link
|
|
73
|
+
for (const m of memoryMdText.matchAll(/\]\(([a-z0-9_.-]+\.md)\)/gi)) {
|
|
74
|
+
refs.add(m[1]);
|
|
75
|
+
}
|
|
76
|
+
return refs;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Validate one memory directory.
|
|
81
|
+
*
|
|
82
|
+
* @returns {{ violations: string[], warnings: string[], stats: object }}
|
|
83
|
+
*/
|
|
84
|
+
export function validateMemoryDir(opts = {}) {
|
|
85
|
+
const memoryDir = opts.memoryDir
|
|
86
|
+
? path.resolve(opts.memoryDir)
|
|
87
|
+
: resolveMemoryDir({ homeDir: opts.homeDir, cwd: opts.cwd, settingsPath: opts.settingsPath });
|
|
88
|
+
|
|
89
|
+
const violations = [];
|
|
90
|
+
const warnings = [];
|
|
91
|
+
const stats = { memoryDir };
|
|
92
|
+
|
|
93
|
+
if (!fs.existsSync(memoryDir)) {
|
|
94
|
+
return {
|
|
95
|
+
violations: [`memory directory does not exist: ${memoryDir}`],
|
|
96
|
+
warnings,
|
|
97
|
+
stats,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const indexPath = path.join(memoryDir, MEMORY_INDEX_FILE);
|
|
102
|
+
if (!fs.existsSync(indexPath)) {
|
|
103
|
+
return {
|
|
104
|
+
violations: [`${MEMORY_INDEX_FILE} missing under ${memoryDir}`],
|
|
105
|
+
warnings,
|
|
106
|
+
stats,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const indexText = fs.readFileSync(indexPath, 'utf8');
|
|
111
|
+
const indexBytes = Buffer.byteLength(indexText, 'utf8');
|
|
112
|
+
const indexLines = indexText.split('\n').length;
|
|
113
|
+
stats.indexLines = indexLines;
|
|
114
|
+
stats.indexBytes = indexBytes;
|
|
115
|
+
|
|
116
|
+
if (indexLines > MEMORY_INDEX_LINE_CAP) {
|
|
117
|
+
violations.push(
|
|
118
|
+
`MEMORY.md exceeds line cap: ${indexLines} lines (max ${MEMORY_INDEX_LINE_CAP}). Reduce index verbosity or move details into topic files.`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
if (indexBytes > MEMORY_INDEX_BYTE_CAP) {
|
|
122
|
+
violations.push(
|
|
123
|
+
`MEMORY.md exceeds byte cap: ${indexBytes} bytes (max ${MEMORY_INDEX_BYTE_CAP}). Reduce index verbosity.`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const referenced = parseMemoryIndex(indexText);
|
|
128
|
+
stats.indexedFileCount = referenced.size;
|
|
129
|
+
|
|
130
|
+
const entries = fs.readdirSync(memoryDir).filter((f) => !f.startsWith('.'));
|
|
131
|
+
const onDiskMd = entries.filter((f) => f.endsWith('.md') && !NON_MEMORY_FILES.has(f));
|
|
132
|
+
stats.onDiskMemoryFileCount = onDiskMd.length;
|
|
133
|
+
|
|
134
|
+
// Orphans on disk (file exists, not indexed).
|
|
135
|
+
for (const f of onDiskMd) {
|
|
136
|
+
if (!referenced.has(f)) {
|
|
137
|
+
violations.push(
|
|
138
|
+
`orphan memory file: ${f} exists in ${memoryDir} but is not referenced by MEMORY.md. ` +
|
|
139
|
+
`Either reference it (manually or via /cc-remember next time) or delete it.`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Broken references (indexed but missing on disk).
|
|
145
|
+
for (const ref of referenced) {
|
|
146
|
+
if (ref === MEMORY_INDEX_FILE) continue;
|
|
147
|
+
if (!entries.includes(ref)) {
|
|
148
|
+
violations.push(
|
|
149
|
+
`broken reference in MEMORY.md: ${ref} is indexed but does not exist on disk.`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Size cap on topic-style files.
|
|
155
|
+
for (const f of onDiskMd) {
|
|
156
|
+
if (!isTopicStyleFile(f)) continue;
|
|
157
|
+
const bytes = fs.statSync(path.join(memoryDir, f)).size;
|
|
158
|
+
if (bytes > TOPIC_FILE_SIZE_CAP) {
|
|
159
|
+
violations.push(
|
|
160
|
+
`topic file too large: ${f} is ${bytes} bytes (cap ${TOPIC_FILE_SIZE_CAP}). Re-shard via migration tool or split manually.`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { violations, warnings, stats };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const isMain = import.meta.url === `file://${process.argv[1]}`;
|
|
169
|
+
if (isMain) {
|
|
170
|
+
const args = process.argv.slice(2);
|
|
171
|
+
let memoryDir = null;
|
|
172
|
+
let quiet = false;
|
|
173
|
+
for (let i = 0; i < args.length; i++) {
|
|
174
|
+
if (args[i] === '--memory-dir' || args[i] === '--dir') memoryDir = args[++i];
|
|
175
|
+
else if (args[i] === '--quiet') quiet = true;
|
|
176
|
+
else if (args[i] === '--help' || args[i] === '-h') {
|
|
177
|
+
console.log('usage: validate-memory.mjs [--memory-dir <path>] [--quiet]');
|
|
178
|
+
console.log(' Validates a Claude Code memory directory. Defaults to the');
|
|
179
|
+
console.log(' current project\'s memory dir. Exits 0 on pass, 1 on violations.');
|
|
180
|
+
process.exit(0);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const { violations, warnings, stats } = validateMemoryDir({ memoryDir });
|
|
185
|
+
if (!quiet) {
|
|
186
|
+
console.log(`memory-dir: ${stats.memoryDir}`);
|
|
187
|
+
if (stats.indexLines !== undefined) {
|
|
188
|
+
console.log(`MEMORY.md: ${stats.indexLines} lines / ${stats.indexBytes} bytes`);
|
|
189
|
+
}
|
|
190
|
+
if (stats.onDiskMemoryFileCount !== undefined) {
|
|
191
|
+
console.log(`on disk: ${stats.onDiskMemoryFileCount} files`);
|
|
192
|
+
console.log(`indexed: ${stats.indexedFileCount} references`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
for (const w of warnings) console.warn(`WARN: ${w}`);
|
|
196
|
+
for (const v of violations) console.error(`FAIL: ${v}`);
|
|
197
|
+
if (violations.length > 0) {
|
|
198
|
+
console.error(`\nvalidate-memory: ${violations.length} violation(s)`);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
if (!quiet) console.log('validate-memory: PASS');
|
|
202
|
+
process.exit(0);
|
|
203
|
+
} catch (e) {
|
|
204
|
+
console.error('validate-memory failed:', e.message);
|
|
205
|
+
process.exit(2);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write a single memory file under the project's memory dir and
|
|
3
|
+
* update MEMORY.md's index to reference it.
|
|
4
|
+
*
|
|
5
|
+
* This is the per-file curated-style writer: each memory is its own
|
|
6
|
+
* .md file with a descriptive slug, matching Claude Code's native
|
|
7
|
+
* auto-memory convention. Used by /cc-remember and (Phase 3b) by
|
|
8
|
+
* debrief's record-lessons phase.
|
|
9
|
+
*
|
|
10
|
+
* Does NOT depend on omega — works against the file-based memory
|
|
11
|
+
* layout regardless of whether omega is installed.
|
|
12
|
+
*
|
|
13
|
+
* @module scripts/write-memory-file
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
19
|
+
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const { resolveMemoryDir } = require('./project-context.cjs');
|
|
22
|
+
|
|
23
|
+
const MEMORY_INDEX_FILE = 'MEMORY.md';
|
|
24
|
+
const CURATED_SECTION_HEADER = '## Curated entries (hand-authored)';
|
|
25
|
+
const CURATED_SECTION_BODY =
|
|
26
|
+
'_Hand-curated memory files. Each is one memory, written by Claude or you. Loaded on demand when Claude references them below._\n';
|
|
27
|
+
|
|
28
|
+
const SLUG_RE = /^[a-z0-9][a-z0-9_-]{0,79}$/;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Normalize an input string to a valid slug.
|
|
32
|
+
* Lowercase, alphanumeric + underscore + hyphen, 1-80 chars,
|
|
33
|
+
* starts with alphanumeric. Strips leading/trailing punctuation.
|
|
34
|
+
*/
|
|
35
|
+
export function normalizeSlug(input) {
|
|
36
|
+
if (!input || typeof input !== 'string') {
|
|
37
|
+
throw new Error('slug: input must be a non-empty string');
|
|
38
|
+
}
|
|
39
|
+
const normalized = input
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.trim()
|
|
42
|
+
.replace(/[^a-z0-9_-]+/g, '_')
|
|
43
|
+
.replace(/^[_-]+|[_-]+$/g, '')
|
|
44
|
+
.slice(0, 80);
|
|
45
|
+
if (!normalized || !SLUG_RE.test(normalized)) {
|
|
46
|
+
throw new Error(`slug "${input}" could not be normalized to a valid filename`);
|
|
47
|
+
}
|
|
48
|
+
return normalized;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Write a memory file. If a file with the same slug exists, append
|
|
53
|
+
* a date suffix until unique (slug.md → slug_2026-05-27.md →
|
|
54
|
+
* slug_2026-05-27-2.md).
|
|
55
|
+
*
|
|
56
|
+
* @param {object} opts
|
|
57
|
+
* @param {string} opts.slug — descriptive identifier for the memory
|
|
58
|
+
* @param {string} opts.content — markdown body
|
|
59
|
+
* @param {string} [opts.title] — optional `# Title` heading; defaults
|
|
60
|
+
* to a title-cased version of the slug
|
|
61
|
+
* @param {string} [opts.description] — one-line description for the
|
|
62
|
+
* MEMORY.md index entry (recommended; defaults to first line of content)
|
|
63
|
+
* @param {string} [opts.memoryDir] — override the resolved memory dir
|
|
64
|
+
* @param {string} [opts.homeDir] — override $HOME (for tests)
|
|
65
|
+
* @param {string} [opts.cwd] — override cwd (for tests)
|
|
66
|
+
* @param {string} [opts.settingsPath] — override settings.json path
|
|
67
|
+
* @param {Date|string} [opts.date] — override "today" for testing
|
|
68
|
+
* @returns {{filePath: string, slug: string, indexed: boolean, bytesWritten: number}}
|
|
69
|
+
*/
|
|
70
|
+
export function writeMemoryFile(opts = {}) {
|
|
71
|
+
if (!opts.content || typeof opts.content !== 'string') {
|
|
72
|
+
throw new Error('writeMemoryFile: content is required and must be a string');
|
|
73
|
+
}
|
|
74
|
+
if (!opts.slug) {
|
|
75
|
+
throw new Error('writeMemoryFile: slug is required (provide via opts.slug or via /cc-remember --slug)');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const slug = normalizeSlug(opts.slug);
|
|
79
|
+
const memoryDir = opts.memoryDir
|
|
80
|
+
? path.resolve(opts.memoryDir)
|
|
81
|
+
: resolveMemoryDir({ homeDir: opts.homeDir, cwd: opts.cwd, settingsPath: opts.settingsPath });
|
|
82
|
+
|
|
83
|
+
fs.mkdirSync(memoryDir, { recursive: true });
|
|
84
|
+
|
|
85
|
+
const date = opts.date ? new Date(opts.date) : new Date();
|
|
86
|
+
const dateStr = date.toISOString().slice(0, 10);
|
|
87
|
+
|
|
88
|
+
// Resolve filename, avoiding collision via date + counter suffix.
|
|
89
|
+
let fileName = `${slug}.md`;
|
|
90
|
+
let counter = 1;
|
|
91
|
+
while (fs.existsSync(path.join(memoryDir, fileName))) {
|
|
92
|
+
counter++;
|
|
93
|
+
fileName = counter === 2
|
|
94
|
+
? `${slug}_${dateStr}.md`
|
|
95
|
+
: `${slug}_${dateStr}-${counter - 1}.md`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const finalSlug = fileName.replace(/\.md$/, '');
|
|
99
|
+
const filePath = path.join(memoryDir, fileName);
|
|
100
|
+
|
|
101
|
+
const title = opts.title || finalSlug.replace(/[_-]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
102
|
+
const body = [`# ${title}`, '', `_Captured: ${dateStr}_`, '', opts.content.trim(), ''].join('\n');
|
|
103
|
+
|
|
104
|
+
// Atomic write via temp + rename.
|
|
105
|
+
const tmpPath = filePath + '.tmp-' + process.pid;
|
|
106
|
+
fs.writeFileSync(tmpPath, body, 'utf8');
|
|
107
|
+
fs.renameSync(tmpPath, filePath);
|
|
108
|
+
|
|
109
|
+
const description = opts.description || extractFirstLine(opts.content) || title;
|
|
110
|
+
const indexed = updateMemoryIndex({ memoryDir, fileName, title, description });
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
filePath,
|
|
114
|
+
slug: finalSlug,
|
|
115
|
+
indexed,
|
|
116
|
+
bytesWritten: Buffer.byteLength(body, 'utf8'),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function extractFirstLine(text) {
|
|
121
|
+
for (const raw of text.split('\n')) {
|
|
122
|
+
const line = raw.trim().replace(/^[-*#>\s]+/, '');
|
|
123
|
+
if (line) return line.slice(0, 100);
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Add an entry for `fileName` to MEMORY.md's curated-entries section.
|
|
130
|
+
* Creates the section if absent. If the file is already indexed
|
|
131
|
+
* anywhere (Topic files OR Curated entries section), no-op.
|
|
132
|
+
*
|
|
133
|
+
* Returns true if MEMORY.md was modified, false if no change needed.
|
|
134
|
+
*/
|
|
135
|
+
function updateMemoryIndex({ memoryDir, fileName, title, description }) {
|
|
136
|
+
const indexPath = path.join(memoryDir, MEMORY_INDEX_FILE);
|
|
137
|
+
let body = fs.existsSync(indexPath) ? fs.readFileSync(indexPath, 'utf8') : '# Memory Index\n\n';
|
|
138
|
+
|
|
139
|
+
// Idempotency: don't re-index a file already referenced anywhere.
|
|
140
|
+
if (body.includes(`(${fileName})`) || body.includes(`**${fileName}**`)) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const entry = `- [${title}](${fileName}) — ${description}`;
|
|
145
|
+
|
|
146
|
+
if (body.includes(CURATED_SECTION_HEADER)) {
|
|
147
|
+
body = body.replace(CURATED_SECTION_HEADER, `${CURATED_SECTION_HEADER}\n${entry}`);
|
|
148
|
+
} else {
|
|
149
|
+
const trailing = body.endsWith('\n') ? '' : '\n';
|
|
150
|
+
body = `${body}${trailing}\n${CURATED_SECTION_HEADER}\n\n${CURATED_SECTION_BODY}\n${entry}\n`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const tmp = indexPath + '.tmp-' + process.pid;
|
|
154
|
+
fs.writeFileSync(tmp, body, 'utf8');
|
|
155
|
+
fs.renameSync(tmp, indexPath);
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// CLI mode: `node scripts/write-memory-file.mjs --slug foo "content..."`
|
|
160
|
+
const isMain = import.meta.url === `file://${process.argv[1]}`;
|
|
161
|
+
if (isMain) {
|
|
162
|
+
const args = process.argv.slice(2);
|
|
163
|
+
let slug = null;
|
|
164
|
+
let title = null;
|
|
165
|
+
let description = null;
|
|
166
|
+
const positional = [];
|
|
167
|
+
for (let i = 0; i < args.length; i++) {
|
|
168
|
+
if (args[i] === '--slug') slug = args[++i];
|
|
169
|
+
else if (args[i] === '--title') title = args[++i];
|
|
170
|
+
else if (args[i] === '--description') description = args[++i];
|
|
171
|
+
else positional.push(args[i]);
|
|
172
|
+
}
|
|
173
|
+
const content = positional.join(' ');
|
|
174
|
+
if (!slug || !content) {
|
|
175
|
+
console.error('usage: write-memory-file.mjs --slug <slug> [--title <title>] [--description <desc>] <content>');
|
|
176
|
+
process.exit(2);
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const result = writeMemoryFile({ slug, content, title, description });
|
|
180
|
+
console.log(JSON.stringify(result, null, 2));
|
|
181
|
+
} catch (e) {
|
|
182
|
+
console.error('write-memory-file failed:', e.message);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -63,7 +63,10 @@ With user confirmation:
|
|
|
63
63
|
1. Update `version` in `package.json`
|
|
64
64
|
2. Commit: `Bump to <version>`
|
|
65
65
|
3. Tag: `git tag v<version>`
|
|
66
|
-
4. `npm publish`
|
|
66
|
+
4. `CC_ALLOW_PUBLISH=1 npm publish` — the `prepublishOnly` guard blocks
|
|
67
|
+
a bare `npm publish` (raw publish on v0.27.1 left every consumer
|
|
68
|
+
stale on v0.26); the env var is the deliberate escape, and reaching
|
|
69
|
+
this step via /cc-publish guarantees Step 6's consumer walk follows.
|
|
67
70
|
5. `git push && git push --tags`
|
|
68
71
|
|
|
69
72
|
### 5. Post-Publish
|
|
@@ -17,6 +17,10 @@ Return true if ANY of:
|
|
|
17
17
|
4. `~/.claude/settings.json.mcpServers` or
|
|
18
18
|
`~/.claude.json.mcpServers` has any key matching
|
|
19
19
|
`/^omega(-memory)?$/i` OR any command containing `omega-venv`
|
|
20
|
+
5. Any inert omega file artifact remains in the project (a migration
|
|
21
|
+
that tore down the venv/hooks/MCP can still leave these on disk):
|
|
22
|
+
`.claude/hooks/omega-memory-guard.sh`, `.claude/hooks/domain-memories.sh`,
|
|
23
|
+
`scripts/cabinet-memory-adapter.py`, `scripts/migrate-memory-to-omega.py`
|
|
20
24
|
|
|
21
25
|
Full detection check:
|
|
22
26
|
|
|
@@ -38,10 +42,35 @@ for p in ['$HOME/.claude/settings.json', '$HOME/.claude.json']:
|
|
|
38
42
|
pass
|
|
39
43
|
raise SystemExit(1)
|
|
40
44
|
" 2>/dev/null && HAS_OMEGA=1
|
|
45
|
+
for f in .claude/hooks/omega-memory-guard.sh .claude/hooks/domain-memories.sh \
|
|
46
|
+
scripts/cabinet-memory-adapter.py scripts/migrate-memory-to-omega.py; do
|
|
47
|
+
test -f "$f" && HAS_OMEGA=1
|
|
48
|
+
done
|
|
41
49
|
```
|
|
42
50
|
|
|
43
51
|
If `HAS_OMEGA=1`, proceed. If 0, skip this phase silently.
|
|
44
52
|
|
|
53
|
+
## Inert-artifact sweep (always runs when this phase proceeds)
|
|
54
|
+
|
|
55
|
+
A consumer whose omega was already migrated (`migrated_from_omega.state
|
|
56
|
+
=== 'complete'`, venv/hooks/MCP gone) can still carry inert omega files
|
|
57
|
+
that the teardown didn't remove. Remove them by exact name. This is
|
|
58
|
+
idempotent — `rm -f` no-ops when the file is absent, so it's safe to
|
|
59
|
+
run on every cc-upgrade:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
rm -f .claude/hooks/omega-memory-guard.sh \
|
|
63
|
+
.claude/hooks/domain-memories.sh \
|
|
64
|
+
scripts/cabinet-memory-adapter.py \
|
|
65
|
+
scripts/migrate-memory-to-omega.py
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Report which files were actually removed (test before/after, or capture
|
|
69
|
+
`rm -v` output). Only remove these exact paths — do not glob or remove
|
|
70
|
+
anything else. If omega is still ACTIVE (conditions 1–4 above), run the
|
|
71
|
+
full migration flow below FIRST; the sweep cleans up what teardown
|
|
72
|
+
leaves behind, it does not replace migration.
|
|
73
|
+
|
|
45
74
|
## User-friendly prompt
|
|
46
75
|
|
|
47
76
|
Default to **dry-run** — a non-Oren user just upgrading CC shouldn't
|