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 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` and `memory`) are opt-in. To add one
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,memory`.
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/domain-memories.sh', 'scripts/cc-drift-check.cjs'],
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.',
@@ -70,6 +70,15 @@ const DEFAULT_HOOKS = {
70
70
  },
71
71
  ],
72
72
  },
73
+ {
74
+ matcher: 'Edit|Write',
75
+ hooks: [
76
+ {
77
+ type: 'command',
78
+ command: '.claude/hooks/memory-index-guard.sh',
79
+ },
80
+ ],
81
+ },
73
82
  ],
74
83
  };
75
84
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-cabinet",
3
- "version": "0.27.1",
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