create-claude-cabinet 0.27.0 → 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/migrate-from-omega.js +143 -13
- package/lib/migrate-memory-cmd.js +11 -6
- 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.',
|
|
@@ -549,6 +549,111 @@ function buildEdgesJson(edges) {
|
|
|
549
549
|
);
|
|
550
550
|
}
|
|
551
551
|
|
|
552
|
+
// ---------------------------------------------------------------------------
|
|
553
|
+
// Merge mode — additive, never clobbers native memory
|
|
554
|
+
// ---------------------------------------------------------------------------
|
|
555
|
+
|
|
556
|
+
const OMEGA_SUBDIR = 'omega-migrated';
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Build the MEMORY.md section that indexes omega topic files living under
|
|
560
|
+
* the omega-migrated/ subdir. Carries the preamble marker so a re-run
|
|
561
|
+
* detects already-migrated. Paths are prefixed with the subdir.
|
|
562
|
+
*/
|
|
563
|
+
function buildMergeSection(topicFileMeta, summary) {
|
|
564
|
+
const lines = [
|
|
565
|
+
PREAMBLE_MARKER,
|
|
566
|
+
'',
|
|
567
|
+
`## Migrated from omega (${new Date().toISOString().slice(0, 10)})`,
|
|
568
|
+
'',
|
|
569
|
+
`_${summary.migrated} memories migrated from omega into \`${OMEGA_SUBDIR}/\`. ` +
|
|
570
|
+
`Native memory files above are unchanged. ${summary.edges} edges in ${OMEGA_SUBDIR}/edges.json._`,
|
|
571
|
+
'',
|
|
572
|
+
];
|
|
573
|
+
for (const { topic, file, count } of topicFileMeta) {
|
|
574
|
+
const desc = describeTopic(topic);
|
|
575
|
+
lines.push(`- [${topic}](${OMEGA_SUBDIR}/${file}) (${count}) — ${desc}`);
|
|
576
|
+
}
|
|
577
|
+
return lines.join('\n') + '\n';
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Additive merge: write omega topic files into an omega-migrated/ subdir
|
|
582
|
+
* (zero filename collision with native files), back up the existing dir
|
|
583
|
+
* first, and append/create a MEMORY.md section indexing them. Never
|
|
584
|
+
* overwrites a native file.
|
|
585
|
+
*
|
|
586
|
+
* @returns {{ backupDir, omegaDir, indexedInto }}
|
|
587
|
+
*/
|
|
588
|
+
function mergeIntoExisting(outputDir, topicFiles, edgesJson, mergeSection, opts = {}) {
|
|
589
|
+
const omegaDir = path.join(outputDir, OMEGA_SUBDIR);
|
|
590
|
+
const memoryMdPath = path.join(outputDir, 'MEMORY.md');
|
|
591
|
+
|
|
592
|
+
// GUARD: never clobber whatever already occupies the subdir path. If it
|
|
593
|
+
// exists, it is either a native item the user named `omega-migrated/`, or
|
|
594
|
+
// residue from an interrupted prior merge. We cannot safely tell them
|
|
595
|
+
// apart, and the cost of guessing wrong is permanent loss of native data.
|
|
596
|
+
// Refuse and instruct — the caller surfaces this as a failure with the
|
|
597
|
+
// native dir fully intact (nothing has been written yet at this point).
|
|
598
|
+
if (fs.existsSync(omegaDir)) {
|
|
599
|
+
throw new Error(
|
|
600
|
+
`Refusing to merge: '${OMEGA_SUBDIR}/' already exists at ${omegaDir}. ` +
|
|
601
|
+
`If it is leftover from an interrupted migration, remove it and re-run. ` +
|
|
602
|
+
`If it is your own data, rename it first, then re-run.`
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// 1. Back up the existing memory dir wholesale, to a guaranteed-fresh path,
|
|
607
|
+
// BEFORE any destructive step. Never proceed without a backup taken
|
|
608
|
+
// this invocation (a stale/reused backup wouldn't reflect pristine
|
|
609
|
+
// native state).
|
|
610
|
+
let backupDir = opts.backupDir;
|
|
611
|
+
if (!backupDir) {
|
|
612
|
+
const stamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-');
|
|
613
|
+
const base = `${outputDir}.pre-omega-merge-${stamp}`;
|
|
614
|
+
backupDir = base;
|
|
615
|
+
for (let n = 1; fs.existsSync(backupDir); n++) backupDir = `${base}-${n}`;
|
|
616
|
+
} else if (fs.existsSync(backupDir)) {
|
|
617
|
+
throw new Error(`Refusing to merge: backup path ${backupDir} already exists.`);
|
|
618
|
+
}
|
|
619
|
+
fs.cpSync(outputDir, backupDir, { recursive: true });
|
|
620
|
+
|
|
621
|
+
// 2. Write omega topic files + edges into the subdir (atomic via staging
|
|
622
|
+
// rename of the subdir). omegaDir is known absent from the guard above.
|
|
623
|
+
const stagingDir = path.join(outputDir, `.${OMEGA_SUBDIR}-staging-${process.pid}`);
|
|
624
|
+
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
625
|
+
fs.mkdirSync(stagingDir, { recursive: true });
|
|
626
|
+
for (const [name, content] of Object.entries(topicFiles)) {
|
|
627
|
+
fs.writeFileSync(path.join(stagingDir, name), content, 'utf8');
|
|
628
|
+
}
|
|
629
|
+
if (edgesJson) fs.writeFileSync(path.join(stagingDir, 'edges.json'), edgesJson, 'utf8');
|
|
630
|
+
fs.renameSync(stagingDir, omegaDir);
|
|
631
|
+
|
|
632
|
+
// 3. Append/create MEMORY.md with the omega section (preamble marker
|
|
633
|
+
// included so re-runs detect already-migrated). Idempotent: if the
|
|
634
|
+
// marker is somehow already present, do not append a second section.
|
|
635
|
+
let indexedInto;
|
|
636
|
+
if (fs.existsSync(memoryMdPath)) {
|
|
637
|
+
const existing = fs.readFileSync(memoryMdPath, 'utf8');
|
|
638
|
+
if (existing.includes(PREAMBLE_MARKER)) {
|
|
639
|
+
indexedInto = 'already-indexed';
|
|
640
|
+
} else {
|
|
641
|
+
const sep = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
642
|
+
const merged = existing + sep + mergeSection;
|
|
643
|
+
const tmp = memoryMdPath + '.tmp-' + process.pid;
|
|
644
|
+
fs.writeFileSync(tmp, merged, 'utf8');
|
|
645
|
+
fs.renameSync(tmp, memoryMdPath);
|
|
646
|
+
indexedInto = 'appended-to-existing';
|
|
647
|
+
}
|
|
648
|
+
} else {
|
|
649
|
+
const header = `# Memory Index\n\n_Native memory files in this directory predate the index; Claude reads them on demand._\n\n`;
|
|
650
|
+
fs.writeFileSync(memoryMdPath, header + mergeSection, 'utf8');
|
|
651
|
+
indexedInto = 'created-new';
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return { backupDir, omegaDir, indexedInto };
|
|
655
|
+
}
|
|
656
|
+
|
|
552
657
|
// ---------------------------------------------------------------------------
|
|
553
658
|
// Main orchestrator
|
|
554
659
|
// ---------------------------------------------------------------------------
|
|
@@ -564,19 +669,17 @@ async function migrateFromOmega(opts = {}) {
|
|
|
564
669
|
const currentProject = opts.currentProject || resolveCurrentProject(cwd, homeDir);
|
|
565
670
|
const outputDir = resolveOutputDir({ ...opts, cwd, homeDir });
|
|
566
671
|
|
|
672
|
+
// Determine write mode. Foreign content (native memory present, no
|
|
673
|
+
// migration preamble) triggers MERGE — additive, never clobbers — so
|
|
674
|
+
// omega memories land alongside native memory instead of being skipped.
|
|
675
|
+
let mode = 'fresh';
|
|
567
676
|
if (!opts.force) {
|
|
568
677
|
const existing = checkExistingMigration(outputDir);
|
|
569
678
|
if (existing.state === 'migrated') {
|
|
570
679
|
return { migrated: 0, reason: 'already-migrated', outputDir };
|
|
571
680
|
}
|
|
572
681
|
if (existing.state === 'foreign-content' || existing.state === 'partial-or-foreign') {
|
|
573
|
-
|
|
574
|
-
migrated: 0,
|
|
575
|
-
reason: existing.state,
|
|
576
|
-
outputDir,
|
|
577
|
-
details: existing.files || ['MEMORY.md without migration preamble'],
|
|
578
|
-
hint: 'Inspect outputDir. If safe to overwrite, re-run with { force: true }.',
|
|
579
|
-
};
|
|
682
|
+
mode = 'merge';
|
|
580
683
|
}
|
|
581
684
|
}
|
|
582
685
|
|
|
@@ -587,6 +690,10 @@ async function migrateFromOmega(opts = {}) {
|
|
|
587
690
|
const { memories, edges } = readVault(vaultDir);
|
|
588
691
|
|
|
589
692
|
if (memories.length === 0) {
|
|
693
|
+
// Nothing to migrate. In merge mode, leave native memory untouched.
|
|
694
|
+
if (mode === 'merge') {
|
|
695
|
+
return { migrated: 0, reason: 'empty-db', outputDir, mode: 'merge', noop: true };
|
|
696
|
+
}
|
|
590
697
|
const minimalIndex = `${PREAMBLE_MARKER}\n# Memory Index\n\n_Source: migrated from omega on ${new Date()
|
|
591
698
|
.toISOString()
|
|
592
699
|
.slice(0, 10)}. No prior memories migrated — omega database was empty._\n`;
|
|
@@ -636,11 +743,7 @@ async function migrateFromOmega(opts = {}) {
|
|
|
636
743
|
}
|
|
637
744
|
|
|
638
745
|
const memoryMd = buildMemoryMd(topicFileMeta, summary);
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
if (edges.length > 0) {
|
|
642
|
-
filesToWrite['edges.json'] = buildEdgesJson(edges);
|
|
643
|
-
}
|
|
746
|
+
const edgesJson = edges.length > 0 ? buildEdgesJson(edges) : null;
|
|
644
747
|
|
|
645
748
|
if (opts.dryRun) {
|
|
646
749
|
return {
|
|
@@ -648,13 +751,39 @@ async function migrateFromOmega(opts = {}) {
|
|
|
648
751
|
edges: edges.length,
|
|
649
752
|
outputDir,
|
|
650
753
|
dryRun: true,
|
|
754
|
+
mode,
|
|
651
755
|
topicFiles: topicFileMeta,
|
|
652
|
-
memoryMdPreview: memoryMd,
|
|
756
|
+
memoryMdPreview: mode === 'merge' ? buildMergeSection(topicFileMeta, summary) : memoryMd,
|
|
653
757
|
omegaBin,
|
|
654
758
|
currentProject,
|
|
655
759
|
};
|
|
656
760
|
}
|
|
657
761
|
|
|
762
|
+
if (mode === 'merge') {
|
|
763
|
+
const mergeSection = buildMergeSection(topicFileMeta, summary);
|
|
764
|
+
const { backupDir, omegaDir, indexedInto } = mergeIntoExisting(
|
|
765
|
+
outputDir,
|
|
766
|
+
filesToWrite,
|
|
767
|
+
edgesJson,
|
|
768
|
+
mergeSection,
|
|
769
|
+
opts
|
|
770
|
+
);
|
|
771
|
+
return {
|
|
772
|
+
migrated: memories.length,
|
|
773
|
+
edges: edges.length,
|
|
774
|
+
outputDir,
|
|
775
|
+
mode: 'merge',
|
|
776
|
+
backupDir,
|
|
777
|
+
omegaDir,
|
|
778
|
+
indexedInto,
|
|
779
|
+
topicFiles: topicFileMeta,
|
|
780
|
+
currentProject,
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Fresh write (empty dir, or force clobber).
|
|
785
|
+
filesToWrite['MEMORY.md'] = memoryMd;
|
|
786
|
+
if (edgesJson) filesToWrite['edges.json'] = edgesJson;
|
|
658
787
|
const stagingDir = path.join(path.dirname(outputDir), STAGING_PREFIX + process.pid);
|
|
659
788
|
writeStaging(stagingDir, filesToWrite);
|
|
660
789
|
commitStaging(stagingDir, outputDir, { force: opts.force });
|
|
@@ -663,6 +792,7 @@ async function migrateFromOmega(opts = {}) {
|
|
|
663
792
|
migrated: memories.length,
|
|
664
793
|
edges: edges.length,
|
|
665
794
|
outputDir,
|
|
795
|
+
mode: 'fresh',
|
|
666
796
|
topicFiles: topicFileMeta,
|
|
667
797
|
currentProject,
|
|
668
798
|
};
|
|
@@ -221,15 +221,20 @@ async function stepMigrateMemories(ctx) {
|
|
|
221
221
|
if (result.reason === 'already-migrated') {
|
|
222
222
|
return { action: `memories already migrated; reusing existing output at ${result.outputDir}` };
|
|
223
223
|
}
|
|
224
|
-
if (result.reason === 'foreign-content' || result.reason === 'partial-or-foreign') {
|
|
225
|
-
return {
|
|
226
|
-
action: `migrate-from-omega refused: ${result.reason} at ${result.outputDir}. ` +
|
|
227
|
-
`Hint: ${result.hint || 'inspect manually'}. Continuing — omega cleanup steps will still run.`,
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
224
|
if (result.reason === 'empty-db') {
|
|
225
|
+
if (result.mode === 'merge') {
|
|
226
|
+
return { action: `omega DB was empty; native memory at ${result.outputDir} left untouched` };
|
|
227
|
+
}
|
|
231
228
|
return { action: `omega DB was empty; minimal MEMORY.md written at ${result.outputDir}` };
|
|
232
229
|
}
|
|
230
|
+
if (result.mode === 'merge') {
|
|
231
|
+
return {
|
|
232
|
+
action:
|
|
233
|
+
`merged ${result.migrated} omega memories (${result.edges || 0} edges) into existing native ` +
|
|
234
|
+
`memory at ${result.outputDir} (omega content under omega-migrated/; backup at ${result.backupDir}). ` +
|
|
235
|
+
`Native memories preserved.`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
233
238
|
return {
|
|
234
239
|
action: `migrated ${result.migrated} memories (${result.edges || 0} edges) → ${result.outputDir}`,
|
|
235
240
|
};
|
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
|