@haposoft/cafekit 0.14.0 → 0.14.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 +27 -3
- package/bin/__tests__/installer-safety.test.js +145 -0
- package/bin/install.js +9 -1
- package/bin/lib/backup.js +60 -16
- package/bin/lib/context.js +1 -1
- package/bin/phases/claude-runtime.js +57 -4
- package/bin/phases/post-install.js +39 -26
- package/bin/phases/root-config.js +24 -5
- package/bin/phases/select-platform.js +14 -9
- package/package.json +1 -1
- package/src/claude/CLAUDE.md +1 -1
- package/src/claude/agents/code-auditor.md +1 -1
- package/src/claude/agents/debugger.md +1 -0
- package/src/claude/agents/god-developer.md +10 -1
- package/src/claude/agents/inspector.md +1 -0
- package/src/claude/agents/spec-maker.md +1 -1
- package/src/claude/agents/test-runner.md +2 -2
- package/src/claude/gitignore +11 -4
- package/src/claude/hooks/__tests__/privacy-block.test.js +50 -10
- package/src/claude/hooks/__tests__/spec-gate.test.js +75 -1
- package/src/claude/hooks/__tests__/state.test.js +65 -0
- package/src/claude/hooks/docs-sync.cjs +15 -21
- package/src/claude/hooks/inspect-block.cjs +1 -1
- package/src/claude/hooks/lib/config.cjs +0 -5
- package/src/claude/hooks/privacy-block.cjs +39 -62
- package/src/claude/hooks/spec-gate.cjs +18 -7
- package/src/claude/hooks/spec-state.cjs +4 -0
- package/src/claude/hooks/state.cjs +13 -3
- package/src/claude/hooks/usage.cjs +6 -0
- package/src/claude/migration-manifest.json +1 -0
- package/src/claude/rules/orchestrator.md +2 -2
- package/src/claude/rules/skill-domain-routing.md +27 -86
- package/src/claude/rules/skill-workflow-routing.md +11 -74
- package/src/claude/rules/state-sync.md +1 -1
- package/src/claude/runtime.json +7 -7
- package/src/claude/scripts/validate-spec-output.cjs +6 -0
- package/src/claude/skills/code-review/references/spec-compliance-review.md +1 -1
- package/src/claude/skills/delegate/SKILL.md +124 -0
- package/src/claude/skills/delegate/references/codex-delegation.md +71 -0
- package/src/claude/skills/delegate/references/grok-delegation.md +84 -0
- package/src/claude/skills/develop/SKILL.md +26 -5
- package/src/claude/skills/develop/references/parallel-waves.md +112 -0
- package/src/claude/skills/develop/references/quality-gate.md +10 -2
- package/src/claude/skills/hotfix/references/parallel-patterns.md +1 -1
- package/src/claude/skills/inspect/SKILL.md +42 -113
- package/src/claude/skills/inspect/references/internal-inspection.md +16 -135
- package/src/claude/skills/specs/SKILL.md +134 -386
- package/src/claude/skills/specs/rules/tasks-generation.md +1 -1
- package/src/claude/skills/specs/templates/task.md +3 -1
- package/src/claude/skills/sync/SKILL.md +2 -2
- package/src/claude/skills/sync/references/sync-protocols.md +4 -4
- package/src/claude/skills/test/SKILL.md +1 -1
- package/src/claude/status.cjs +9 -6
- package/src/opencode/plugins/docs-sync.ts +10 -16
- package/src/claude/archive-command/code/SKILL.md +0 -55
- package/src/claude/archive-command/code/references/execution-loop.md +0 -21
- package/src/claude/archive-command/code.md +0 -17
- package/src/claude/archive-command/docs.md +0 -609
- package/src/claude/archive-command/review/codebase/parallel.md +0 -76
- package/src/claude/archive-command/spec-design.md +0 -247
- package/src/claude/archive-command/spec-init.md +0 -120
- package/src/claude/archive-command/spec-requirements.md +0 -138
- package/src/claude/archive-command/spec-status.md +0 -98
- package/src/claude/archive-command/spec-tasks.md +0 -173
- package/src/claude/archive-command/spec-validate.md +0 -118
- package/src/claude/archive-command/test.md +0 -8
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Claude Code-first spec-driven workflow and runtime bundle for AI coding assistants.
|
|
4
4
|
|
|
5
|
-
[](https://github.com/haposoft/cafekit)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://claude.ai/code)
|
|
8
8
|
|
|
@@ -46,6 +46,26 @@ Requirements:
|
|
|
46
46
|
- Node.js 18+
|
|
47
47
|
- Claude Code project with `.claude/`, OpenCode project with `.opencode/` or `opencode.json`, or choose a runtime when prompted
|
|
48
48
|
|
|
49
|
+
## Git ignore policy
|
|
50
|
+
|
|
51
|
+
On install, CafeKit updates the **project-root** `.gitignore` with:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
# CafeKit / Ecosystem
|
|
55
|
+
specs/_shared/
|
|
56
|
+
plans/
|
|
57
|
+
!plans/templates/
|
|
58
|
+
.cafekit-backup/
|
|
59
|
+
.cafekit.lock
|
|
60
|
+
.claude/
|
|
61
|
+
.opencode/
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Runtime folders are local — reinstall with `npx @haposoft/cafekit` rather than
|
|
65
|
+
committing them. Inside the runtime, CafeKit also installs `.claude/.gitignore`
|
|
66
|
+
(or `.opencode/.gitignore`) so secrets, skill dependencies, session state, and
|
|
67
|
+
hook logs stay out of git even if someone force-adds the folder.
|
|
68
|
+
|
|
49
69
|
## What Gets Installed
|
|
50
70
|
|
|
51
71
|
Claude Code targets:
|
|
@@ -62,10 +82,14 @@ Claude Code targets:
|
|
|
62
82
|
├── cafekit.json
|
|
63
83
|
├── status.cjs
|
|
64
84
|
├── runtime.json
|
|
65
|
-
|
|
66
|
-
|
|
85
|
+
└── settings.json
|
|
86
|
+
|
|
87
|
+
CLAUDE.md
|
|
67
88
|
```
|
|
68
89
|
|
|
90
|
+
CafeKit owns only its marked block inside the project-root `CLAUDE.md`; project
|
|
91
|
+
instructions outside that block are preserved across installs and upgrades.
|
|
92
|
+
|
|
69
93
|
OpenCode targets:
|
|
70
94
|
|
|
71
95
|
```text
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { test } = require('node:test');
|
|
8
|
+
|
|
9
|
+
const backup = require('../lib/backup');
|
|
10
|
+
const { copyClaudeMdFile } = require('../phases/claude-runtime');
|
|
11
|
+
const { configureAddressing } = require('../phases/post-install');
|
|
12
|
+
|
|
13
|
+
const START = '<!-- CAFEKIT CLAUDE START -->';
|
|
14
|
+
const END = '<!-- CAFEKIT CLAUDE END -->';
|
|
15
|
+
const TEMPLATE = fs.readFileSync(path.join(__dirname, '../../src/claude/CLAUDE.md'), 'utf8');
|
|
16
|
+
|
|
17
|
+
function inTempProject(run) {
|
|
18
|
+
const originalCwd = process.cwd();
|
|
19
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-installer-safety-'));
|
|
20
|
+
process.chdir(root);
|
|
21
|
+
try {
|
|
22
|
+
return run(root);
|
|
23
|
+
} finally {
|
|
24
|
+
process.chdir(originalCwd);
|
|
25
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function installContext(pruned) {
|
|
30
|
+
return {
|
|
31
|
+
dryRun: false,
|
|
32
|
+
options: { forceOverwrite: true },
|
|
33
|
+
ownership: { '.claude': { files: {} } },
|
|
34
|
+
trackers: {
|
|
35
|
+
claude: {
|
|
36
|
+
recorded: () => null,
|
|
37
|
+
record: () => assert.fail('root CLAUDE.md must not be recorded'),
|
|
38
|
+
prune: (target) => pruned.push(target)
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
ui: { detail() {}, warn() {}, success() {} },
|
|
42
|
+
results: {
|
|
43
|
+
copied: 0,
|
|
44
|
+
updated: 0,
|
|
45
|
+
unchanged: 0,
|
|
46
|
+
preserved: 0,
|
|
47
|
+
missingDependencies: 0,
|
|
48
|
+
preservedFiles: []
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
test('managed CLAUDE block replacement preserves user bytes under force overwrite', () => {
|
|
54
|
+
inTempProject(() => {
|
|
55
|
+
const before = '# User preface\n\n';
|
|
56
|
+
const after = '\n\n## User tail\nKeep this exactly.\n';
|
|
57
|
+
fs.writeFileSync('CLAUDE.md', `${before}${START}\nold managed text\n${END}${after}`);
|
|
58
|
+
|
|
59
|
+
const pruned = [];
|
|
60
|
+
copyClaudeMdFile(installContext(pruned), 'claude');
|
|
61
|
+
|
|
62
|
+
const content = fs.readFileSync('CLAUDE.md', 'utf8');
|
|
63
|
+
assert.ok(content.startsWith(before));
|
|
64
|
+
assert.ok(content.endsWith(after));
|
|
65
|
+
assert.ok(content.includes(TEMPLATE.trimEnd()));
|
|
66
|
+
assert.equal(content.match(new RegExp(START, 'g')).length, 1);
|
|
67
|
+
assert.deepEqual(pruned, ['../CLAUDE.md']);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('legacy pristine CLAUDE template migrates to one managed block', () => {
|
|
72
|
+
inTempProject(() => {
|
|
73
|
+
fs.writeFileSync('CLAUDE.md', TEMPLATE);
|
|
74
|
+
const pruned = [];
|
|
75
|
+
|
|
76
|
+
copyClaudeMdFile(installContext(pruned), 'claude');
|
|
77
|
+
|
|
78
|
+
const content = fs.readFileSync('CLAUDE.md', 'utf8');
|
|
79
|
+
assert.equal(content, `${START}\n${TEMPLATE.trimEnd()}\n${END}\n`);
|
|
80
|
+
assert.equal(content.match(new RegExp(START, 'g')).length, 1);
|
|
81
|
+
assert.deepEqual(pruned, ['../CLAUDE.md']);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('addressing changes remain inside the managed CLAUDE block', () => {
|
|
86
|
+
inTempProject(() => {
|
|
87
|
+
const userSection = '## Addressing (Context Overflow Indicator)\n\nUser-owned wording.';
|
|
88
|
+
const managedSection = '## Addressing (Context Overflow Indicator)\n\nOld managed wording.';
|
|
89
|
+
fs.writeFileSync(
|
|
90
|
+
'CLAUDE.md',
|
|
91
|
+
`${userSection}\n\n${START}\n${managedSection}\n${END}\n`
|
|
92
|
+
);
|
|
93
|
+
const ctx = {
|
|
94
|
+
platforms: ['claude'],
|
|
95
|
+
ui: { success() {}, warn() {} },
|
|
96
|
+
t: () => 'updated'
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
configureAddressing(ctx, 'anh');
|
|
100
|
+
|
|
101
|
+
const content = fs.readFileSync('CLAUDE.md', 'utf8');
|
|
102
|
+
assert.ok(content.startsWith(`${userSection}\n\n${START}`));
|
|
103
|
+
assert.ok(content.includes('Claude Code always addresses the user as "anh"'));
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('backup restores existing targets and removes targets absent before the run', () => {
|
|
108
|
+
inTempProject(() => {
|
|
109
|
+
fs.mkdirSync('.claude/nested', { recursive: true });
|
|
110
|
+
fs.writeFileSync('.claude/nested/runtime.txt', 'before\n');
|
|
111
|
+
fs.writeFileSync('CLAUDE.md', 'user instructions\n');
|
|
112
|
+
|
|
113
|
+
const backupDir = backup.snapshot(
|
|
114
|
+
['.claude', 'CLAUDE.md', '.gitignore'],
|
|
115
|
+
'20260729-test'
|
|
116
|
+
);
|
|
117
|
+
const metadata = JSON.parse(
|
|
118
|
+
fs.readFileSync(path.join(backupDir, 'snapshot.json'), 'utf8')
|
|
119
|
+
);
|
|
120
|
+
assert.deepEqual(metadata.targets, [
|
|
121
|
+
{ target: '.claude', existed: true },
|
|
122
|
+
{ target: 'CLAUDE.md', existed: true },
|
|
123
|
+
{ target: '.gitignore', existed: false }
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
fs.writeFileSync('.claude/nested/runtime.txt', 'after\n');
|
|
127
|
+
fs.writeFileSync('CLAUDE.md', 'overwritten\n');
|
|
128
|
+
fs.writeFileSync('.gitignore', 'generated\n');
|
|
129
|
+
backup.restore(backupDir);
|
|
130
|
+
|
|
131
|
+
assert.equal(fs.readFileSync('.claude/nested/runtime.txt', 'utf8'), 'before\n');
|
|
132
|
+
assert.equal(fs.readFileSync('CLAUDE.md', 'utf8'), 'user instructions\n');
|
|
133
|
+
assert.equal(fs.existsSync('.gitignore'), false);
|
|
134
|
+
assert.equal(fs.existsSync(path.join(backupDir, 'snapshot.json')), true);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('backup rejects traversal targets before snapshot or restore deletion', () => {
|
|
139
|
+
inTempProject(() => {
|
|
140
|
+
assert.throws(
|
|
141
|
+
() => backup.snapshot(['../outside'], '20260729-test'),
|
|
142
|
+
/Unsafe backup target/
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
});
|
package/bin/install.js
CHANGED
|
@@ -170,11 +170,19 @@ async function main() {
|
|
|
170
170
|
await runPostInstall(ctx);
|
|
171
171
|
await setupSkillDeps(ctx);
|
|
172
172
|
await setupRtk(ctx);
|
|
173
|
+
|
|
174
|
+
// Phase handlers may report recoverable-looking errors through counters
|
|
175
|
+
// instead of throwing. Treat them as transactional failure so the snapshot
|
|
176
|
+
// is restored before any success summary or backup pruning occurs.
|
|
177
|
+
if (ctx.results.errors > 0) {
|
|
178
|
+
throw new Error(`Installer reported ${ctx.results.errors} error${ctx.results.errors === 1 ? '' : 's'}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
173
181
|
printSummary(ctx);
|
|
174
182
|
|
|
175
183
|
if (!ctx.dryRun) backup.prune(3);
|
|
176
184
|
|
|
177
|
-
exitCode =
|
|
185
|
+
exitCode = 0;
|
|
178
186
|
} catch (error) {
|
|
179
187
|
const log = ctx && ctx.ui ? ctx.ui : { error: (m) => console.error(m) };
|
|
180
188
|
const t = ctx ? ctx.t : (k) => k;
|
package/bin/lib/backup.js
CHANGED
|
@@ -14,6 +14,33 @@ const fs = require('fs');
|
|
|
14
14
|
const path = require('path');
|
|
15
15
|
|
|
16
16
|
const BACKUP_ROOT = '.cafekit-backup';
|
|
17
|
+
const SNAPSHOT_METADATA = 'snapshot.json';
|
|
18
|
+
const SNAPSHOT_DATA = 'data';
|
|
19
|
+
|
|
20
|
+
function validateTarget(target) {
|
|
21
|
+
if (typeof target !== 'string' || !target || target.includes('\0')) {
|
|
22
|
+
throw new Error('Backup target must be a non-empty relative path');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Validate both separator styles so a snapshot created on one platform
|
|
26
|
+
// cannot become unsafe when restored on another.
|
|
27
|
+
const portable = target.replace(/\\/g, '/');
|
|
28
|
+
const parts = portable.split('/');
|
|
29
|
+
if (path.isAbsolute(target) || portable.startsWith('/') || parts.includes('..') || parts.includes('')) {
|
|
30
|
+
throw new Error(`Unsafe backup target: ${target}`);
|
|
31
|
+
}
|
|
32
|
+
if (portable === '.' || portable === BACKUP_ROOT || portable.startsWith(`${BACKUP_ROOT}/`)) {
|
|
33
|
+
throw new Error(`Unsafe backup target: ${target}`);
|
|
34
|
+
}
|
|
35
|
+
return parts.join(path.sep);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function validateRunId(runId) {
|
|
39
|
+
if (typeof runId !== 'string' || !runId || runId === '.' || runId === '..' || /[\\/]/.test(runId)) {
|
|
40
|
+
throw new Error('Backup runId must be a single path segment');
|
|
41
|
+
}
|
|
42
|
+
return runId;
|
|
43
|
+
}
|
|
17
44
|
|
|
18
45
|
/**
|
|
19
46
|
* Recursively copy a directory tree (verbatim, no transforms). Used for
|
|
@@ -40,42 +67,59 @@ function copyTree(src, dest) {
|
|
|
40
67
|
}
|
|
41
68
|
|
|
42
69
|
/**
|
|
43
|
-
* Snapshot
|
|
44
|
-
*
|
|
45
|
-
* null if nothing existed to back up (fresh install → no rollback target).
|
|
70
|
+
* Snapshot explicit targets (relative to cwd, e.g. ['.claude', 'CLAUDE.md']).
|
|
71
|
+
* Every target is recorded, including targets absent before the run.
|
|
46
72
|
*
|
|
47
73
|
* @param {string[]} folders
|
|
48
74
|
* @param {string} runId unique id for this run (timestamp-based)
|
|
49
75
|
*/
|
|
50
76
|
function snapshot(folders, runId) {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const backupDir = path.join(BACKUP_ROOT, runId);
|
|
77
|
+
const targets = [...new Set(folders.map(validateTarget))];
|
|
78
|
+
const backupDir = path.join(BACKUP_ROOT, validateRunId(runId));
|
|
79
|
+
const dataDir = path.join(backupDir, SNAPSHOT_DATA);
|
|
55
80
|
fs.mkdirSync(backupDir, { recursive: true });
|
|
56
81
|
|
|
57
|
-
|
|
58
|
-
|
|
82
|
+
const records = [];
|
|
83
|
+
for (const target of targets) {
|
|
84
|
+
const existed = fs.existsSync(target);
|
|
85
|
+
records.push({ target, existed });
|
|
86
|
+
if (existed) copyTree(target, path.join(dataDir, target));
|
|
59
87
|
}
|
|
88
|
+
fs.writeFileSync(
|
|
89
|
+
path.join(backupDir, SNAPSHOT_METADATA),
|
|
90
|
+
`${JSON.stringify({ schemaVersion: 1, targets: records }, null, 2)}\n`,
|
|
91
|
+
'utf8'
|
|
92
|
+
);
|
|
60
93
|
|
|
61
94
|
return backupDir;
|
|
62
95
|
}
|
|
63
96
|
|
|
64
97
|
/**
|
|
65
|
-
* Restore
|
|
66
|
-
*
|
|
67
|
-
* snapshot, returning the tree to its pre-run state.
|
|
98
|
+
* Restore every explicit target to its pre-run state. Existing targets are
|
|
99
|
+
* replaced from backup; targets absent before the run are removed.
|
|
68
100
|
*
|
|
69
101
|
* @param {string} backupDir path returned by snapshot()
|
|
70
102
|
*/
|
|
71
103
|
function restore(backupDir) {
|
|
72
104
|
if (!backupDir || !fs.existsSync(backupDir)) return;
|
|
73
105
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
106
|
+
const metadataPath = path.join(backupDir, SNAPSHOT_METADATA);
|
|
107
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
|
|
108
|
+
if (metadata?.schemaVersion !== 1 || !Array.isArray(metadata.targets)) {
|
|
109
|
+
throw new Error(`Invalid backup metadata: ${metadataPath}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const record of metadata.targets) {
|
|
113
|
+
if (!record || typeof record.existed !== 'boolean') {
|
|
114
|
+
throw new Error(`Invalid backup target metadata: ${metadataPath}`);
|
|
115
|
+
}
|
|
116
|
+
const target = validateTarget(record.target);
|
|
117
|
+
const source = path.join(backupDir, SNAPSHOT_DATA, target);
|
|
118
|
+
if (record.existed && !fs.existsSync(source)) {
|
|
119
|
+
throw new Error(`Backup data missing for target: ${target}`);
|
|
120
|
+
}
|
|
77
121
|
fs.rmSync(target, { recursive: true, force: true });
|
|
78
|
-
copyTree(source, target);
|
|
122
|
+
if (record.existed) copyTree(source, target);
|
|
79
123
|
}
|
|
80
124
|
}
|
|
81
125
|
|
package/bin/lib/context.js
CHANGED
|
@@ -93,7 +93,7 @@ const PLATFORMS = {
|
|
|
93
93
|
skillsRef: '.opencode/skills',
|
|
94
94
|
commandPrefix: '/',
|
|
95
95
|
sourceDir: 'claude',
|
|
96
|
-
sourceSubdir: '
|
|
96
|
+
sourceSubdir: 'commands' // Dead config (manifest commands.core is []); kept for shape parity
|
|
97
97
|
}
|
|
98
98
|
// Add new platforms here:
|
|
99
99
|
// cursor: {
|
|
@@ -15,11 +15,50 @@ const {
|
|
|
15
15
|
getRuntimeSupportTargetDir,
|
|
16
16
|
getCopyOptions
|
|
17
17
|
} = require('../lib/context');
|
|
18
|
+
const { sha256 } = require('../lib/manifest');
|
|
18
19
|
const { writeManagedFile, copyManagedTree } = require('../lib/managed-writer');
|
|
19
20
|
const { report, treeAction } = require('./report');
|
|
20
21
|
const { normalizeOpenCodeBody } = require('../lib/opencode-install');
|
|
21
22
|
|
|
22
23
|
const SRC = path.join(__dirname, '../../src');
|
|
24
|
+
const CLAUDE_START = '<!-- CAFEKIT CLAUDE START -->';
|
|
25
|
+
const CLAUDE_END = '<!-- CAFEKIT CLAUDE END -->';
|
|
26
|
+
|
|
27
|
+
function managedClaudeBlock(template) {
|
|
28
|
+
return `${CLAUDE_START}\n${template.trimEnd()}\n${CLAUDE_END}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function managedClaudeRange(content) {
|
|
32
|
+
const start = content.indexOf(CLAUDE_START);
|
|
33
|
+
if (start === -1) return null;
|
|
34
|
+
const endStart = content.indexOf(CLAUDE_END, start + CLAUDE_START.length);
|
|
35
|
+
if (endStart === -1) return null;
|
|
36
|
+
return { start, end: endStart + CLAUDE_END.length, bodyStart: start + CLAUDE_START.length, bodyEnd: endStart };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Transform only the CafeKit-owned body, preserving all surrounding bytes. */
|
|
40
|
+
function transformManagedClaudeContent(content, transform) {
|
|
41
|
+
const range = managedClaudeRange(content);
|
|
42
|
+
if (!range) return content;
|
|
43
|
+
const body = content.slice(range.bodyStart, range.bodyEnd);
|
|
44
|
+
return `${content.slice(0, range.bodyStart)}${transform(body)}${content.slice(range.bodyEnd)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function upsertManagedClaudeBlock(existing, template, legacyOwned) {
|
|
48
|
+
const block = managedClaudeBlock(template);
|
|
49
|
+
const range = managedClaudeRange(existing);
|
|
50
|
+
if (range) {
|
|
51
|
+
return `${existing.slice(0, range.start)}${block}${existing.slice(range.end)}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Whole-file migration is deliberately limited to exact shipped content or
|
|
55
|
+
// a file whose bytes still match the legacy ownership manifest.
|
|
56
|
+
if (existing === template || legacyOwned) return `${block}\n`;
|
|
57
|
+
if (!existing) return `${block}\n`;
|
|
58
|
+
|
|
59
|
+
const separator = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
60
|
+
return `${existing}${separator}${block}\n`;
|
|
61
|
+
}
|
|
23
62
|
|
|
24
63
|
/** Copy ROUTING.md (SKILLS_DIR substituted; OpenCode body normalized). */
|
|
25
64
|
function copyRoutingFile(ctx, platformKey) {
|
|
@@ -105,9 +144,22 @@ function copyClaudeMdFile(ctx, platformKey) {
|
|
|
105
144
|
return;
|
|
106
145
|
}
|
|
107
146
|
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
147
|
+
const tracker = ctx.trackers.claude;
|
|
148
|
+
// Older releases recorded the root file relative to .claude/. Never retain
|
|
149
|
+
// that whole-file ownership claim, even when this run makes no content change.
|
|
150
|
+
if (tracker) tracker.prune('../CLAUDE.md');
|
|
151
|
+
|
|
152
|
+
const template = fs.readFileSync(src, 'utf8');
|
|
153
|
+
const exists = fs.existsSync(dest);
|
|
154
|
+
const existing = exists ? fs.readFileSync(dest, 'utf8') : '';
|
|
155
|
+
const legacyRecord = ctx.ownership?.[PLATFORMS.claude.folder]?.files?.['../CLAUDE.md'];
|
|
156
|
+
const legacyOwned = Boolean(legacyRecord && legacyRecord.sha256 === sha256(existing));
|
|
157
|
+
const next = upsertManagedClaudeBlock(existing, template, legacyOwned);
|
|
158
|
+
const action = !exists ? 'created' : next === existing ? 'unchanged' : 'updated';
|
|
159
|
+
|
|
160
|
+
if (!ctx.dryRun && action !== 'unchanged') {
|
|
161
|
+
fs.writeFileSync(dest, next, 'utf8');
|
|
162
|
+
}
|
|
111
163
|
report(ctx, action, 'CLAUDE.md');
|
|
112
164
|
}
|
|
113
165
|
|
|
@@ -137,5 +189,6 @@ module.exports = {
|
|
|
137
189
|
copyClaudeRuntimeFiles,
|
|
138
190
|
removeObsoleteClaudeRuntimeFiles,
|
|
139
191
|
copyClaudeMdFile,
|
|
140
|
-
copyRulesDirectory
|
|
192
|
+
copyRulesDirectory,
|
|
193
|
+
transformManagedClaudeContent
|
|
141
194
|
};
|
|
@@ -16,6 +16,25 @@ const path = require('path');
|
|
|
16
16
|
const { PLATFORMS } = require('../lib/context');
|
|
17
17
|
const { LANGUAGE_LABELS } = require('../lib/i18n');
|
|
18
18
|
const { setupOpenCodeModel } = require('../lib/opencode-install');
|
|
19
|
+
const { transformManagedClaudeContent } = require('./claude-runtime');
|
|
20
|
+
|
|
21
|
+
function updateManagedClaudeFile(filePath, transform) {
|
|
22
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
23
|
+
const next = transformManagedClaudeContent(content, transform);
|
|
24
|
+
if (next === content) return false;
|
|
25
|
+
fs.writeFileSync(filePath, next, 'utf8');
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readManagedClaudeBody(filePath) {
|
|
30
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
31
|
+
let body = '';
|
|
32
|
+
transformManagedClaudeContent(content, (managed) => {
|
|
33
|
+
body = managed;
|
|
34
|
+
return managed;
|
|
35
|
+
});
|
|
36
|
+
return body;
|
|
37
|
+
}
|
|
19
38
|
|
|
20
39
|
/** Write `"language"` field into .claude/settings.json so it's visible at a glance. */
|
|
21
40
|
function patchSettingsLanguage(ctx) {
|
|
@@ -64,20 +83,14 @@ Always respond in **${label}**. Technical terms, code identifiers, and file path
|
|
|
64
83
|
|
|
65
84
|
`;
|
|
66
85
|
|
|
67
|
-
|
|
68
|
-
// Replace if marker present, else replace the generic section.
|
|
86
|
+
// Replace only inside the installer-owned block.
|
|
69
87
|
const markerRe = /## Language Consistency <!-- cafekit:lang -->[\s\S]*?(?=\n##|\n*$)/;
|
|
70
88
|
const genericRe = /## Language Consistency\n[\s\S]*?(?=\n##|\n*$)/;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
} else {
|
|
77
|
-
content += `\n${newSection}\n`;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
fs.writeFileSync(claudeMdFile, content, 'utf8');
|
|
89
|
+
updateManagedClaudeFile(claudeMdFile, (managed) => {
|
|
90
|
+
if (markerRe.test(managed)) return managed.replace(markerRe, newSection);
|
|
91
|
+
if (genericRe.test(managed)) return managed.replace(genericRe, newSection);
|
|
92
|
+
return `${managed.trimEnd()}\n\n${newSection}\n`;
|
|
93
|
+
});
|
|
81
94
|
}
|
|
82
95
|
function patchRuntimeLocale(ctx) {
|
|
83
96
|
for (const key of ctx.platforms) {
|
|
@@ -93,6 +106,10 @@ function patchRuntimeLocale(ctx) {
|
|
|
93
106
|
// Use locale (freeform label) so custom languages propagate to the AI hook.
|
|
94
107
|
const locale = ctx.locale || ctx.lang;
|
|
95
108
|
if (data.locale.responseLanguage === locale) continue;
|
|
109
|
+
// Hardening: never downgrade an existing configured label to a bare default
|
|
110
|
+
// code when this run never made an explicit language choice (ctx.locale
|
|
111
|
+
// empty). Protects user config on any code path that skips selectLanguage.
|
|
112
|
+
if (!ctx.locale && data.locale.responseLanguage) continue;
|
|
96
113
|
data.locale.responseLanguage = locale;
|
|
97
114
|
fs.writeFileSync(rtPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
98
115
|
if (ctx.trackers[key]) ctx.trackers[key].record(rtPath);
|
|
@@ -112,19 +129,18 @@ function configureAddressing(ctx, userAddress) {
|
|
|
112
129
|
}
|
|
113
130
|
|
|
114
131
|
const name = assistantName(ctx);
|
|
115
|
-
let content = fs.readFileSync(claudeMdFile, 'utf8');
|
|
116
132
|
const addressingSection = `## Addressing (Context Overflow Indicator)
|
|
117
133
|
|
|
118
134
|
${name} always addresses the user as "${userAddress}" throughout the conversation. If it stops doing so, it is a sign the context has been compacted/truncated — tell the user to consider \`/clear\`.`;
|
|
119
135
|
|
|
120
|
-
// Idempotent
|
|
136
|
+
// Idempotent and scoped to the installer-owned block.
|
|
121
137
|
const regex = /##[^\n]*Context Overflow Indicator[^\n]*[\s\S]*?(?=\n##|\n*$)/;
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
ctx.ui.success(ctx.t('addressingSet', { name, addr: userAddress }));
|
|
138
|
+
const changed = updateManagedClaudeFile(claudeMdFile, (managed) => (
|
|
139
|
+
regex.test(managed)
|
|
140
|
+
? managed.replace(regex, addressingSection)
|
|
141
|
+
: `${managed.endsWith('\n') ? managed : `${managed}\n`}\n${addressingSection}\n`
|
|
142
|
+
));
|
|
143
|
+
if (changed) ctx.ui.success(ctx.t('addressingSet', { name, addr: userAddress }));
|
|
128
144
|
}
|
|
129
145
|
|
|
130
146
|
async function setupAddressing(ctx) {
|
|
@@ -134,7 +150,7 @@ async function setupAddressing(ctx) {
|
|
|
134
150
|
const claudeMdFile = path.join(process.cwd(), 'CLAUDE.md');
|
|
135
151
|
let existingName = null;
|
|
136
152
|
if (fs.existsSync(claudeMdFile)) {
|
|
137
|
-
const content =
|
|
153
|
+
const content = readManagedClaudeBody(claudeMdFile);
|
|
138
154
|
const match = content.match(/##[^\n]*Context Overflow Indicator[^\n]*[\s\S]*?always addresses the user as "([^"]+)"/);
|
|
139
155
|
if (match) {
|
|
140
156
|
existingName = match[1];
|
|
@@ -199,11 +215,8 @@ async function runPostInstall(ctx) {
|
|
|
199
215
|
// Write "language" field into .claude/settings.json for visibility.
|
|
200
216
|
patchSettingsLanguage(ctx);
|
|
201
217
|
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
if (ctx.platforms.includes('claude') && ctx.trackers.claude && fs.existsSync('CLAUDE.md')) {
|
|
205
|
-
ctx.trackers.claude.record('CLAUDE.md');
|
|
206
|
-
}
|
|
218
|
+
// Flush touched platform trackers. Root CLAUDE.md is block-managed and must
|
|
219
|
+
// never regain a whole-file ownership record.
|
|
207
220
|
for (const key of ctx.platforms) {
|
|
208
221
|
if (ctx.trackers[key]) ctx.trackers[key].write();
|
|
209
222
|
}
|
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Phase: root project configuration (.gitignore).
|
|
3
3
|
*
|
|
4
|
-
* Ensures the root .gitignore carries CafeKit-managed patterns
|
|
5
|
-
*
|
|
4
|
+
* Ensures the root .gitignore carries CafeKit-managed patterns:
|
|
5
|
+
* - workflow dirs (plans, shared research)
|
|
6
|
+
* - install safety artifacts (backup, lock)
|
|
7
|
+
* - runtime folders (.claude/, .opencode/) — reinstall via
|
|
8
|
+
* `npx @haposoft/cafekit`; do not commit the local payload
|
|
9
|
+
*
|
|
10
|
+
* Layered with the in-folder `.claude/.gitignore` /
|
|
11
|
+
* `.opencode/.gitignore` templates (secrets, skill deps, session
|
|
12
|
+
* state) so force-adds and partial un-ignores stay safe.
|
|
13
|
+
*
|
|
14
|
+
* Honors dry-run.
|
|
6
15
|
*/
|
|
7
16
|
|
|
8
17
|
const fs = require('fs');
|
|
9
18
|
const path = require('path');
|
|
10
19
|
|
|
20
|
+
/** True if `lines` already carries `pattern` with or without a trailing slash. */
|
|
21
|
+
function hasPattern(lines, pattern) {
|
|
22
|
+
const bare = pattern.replace(/\/$/, '');
|
|
23
|
+
const withSlash = bare + '/';
|
|
24
|
+
return lines.includes(pattern) || lines.includes(bare) || lines.includes(withSlash);
|
|
25
|
+
}
|
|
26
|
+
|
|
11
27
|
function ensureGitignore(ctx) {
|
|
12
28
|
const gitignorePath = path.join(process.cwd(), '.gitignore');
|
|
13
29
|
const header = '# CafeKit / Ecosystem';
|
|
@@ -16,7 +32,10 @@ function ensureGitignore(ctx) {
|
|
|
16
32
|
'plans/',
|
|
17
33
|
'!plans/templates/',
|
|
18
34
|
'.cafekit-backup/',
|
|
19
|
-
'.cafekit.lock'
|
|
35
|
+
'.cafekit.lock',
|
|
36
|
+
// Local runtime payload — reinstall with npx; keep out of git
|
|
37
|
+
'.claude/',
|
|
38
|
+
'.opencode/'
|
|
20
39
|
];
|
|
21
40
|
const prefix = ctx.dryRun ? '[dry-run] ' : '';
|
|
22
41
|
|
|
@@ -30,7 +49,7 @@ function ensureGitignore(ctx) {
|
|
|
30
49
|
|
|
31
50
|
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
32
51
|
const lines = content.split('\n').map((l) => l.trim());
|
|
33
|
-
const missing = patterns.filter((p) => !lines
|
|
52
|
+
const missing = patterns.filter((p) => !hasPattern(lines, p));
|
|
34
53
|
|
|
35
54
|
if (missing.length > 0) {
|
|
36
55
|
let newContent = content;
|
|
@@ -46,4 +65,4 @@ function ensureGitignore(ctx) {
|
|
|
46
65
|
}
|
|
47
66
|
}
|
|
48
67
|
|
|
49
|
-
module.exports = { ensureGitignore };
|
|
68
|
+
module.exports = { ensureGitignore, hasPattern };
|
|
@@ -35,17 +35,22 @@ function getInstalledLocale() {
|
|
|
35
35
|
|
|
36
36
|
/** First step: pick the installer/UI language (interactive only). */
|
|
37
37
|
async function selectLanguage(ctx) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
38
|
+
// Restore the saved locale BEFORE the interactivity check: a non-interactive
|
|
39
|
+
// upgrade (--yes) must not forget the configured language. Skipping this left
|
|
40
|
+
// ctx at the 'en' default, and patchRuntimeLocale then clobbered the user's
|
|
41
|
+
// responseLanguage on every upgrade.
|
|
42
|
+
if (!ctx.options.lang) {
|
|
43
|
+
const savedLocale = getInstalledLocale();
|
|
44
|
+
if (savedLocale) {
|
|
45
|
+
const code = Object.keys(LANGUAGE_LABELS).find((k) => LANGUAGE_LABELS[k] === savedLocale) || 'en';
|
|
46
|
+
ctx.setLang(code, savedLocale); // updates ctx.t to the saved language
|
|
47
|
+
if (ctx.interactive) ctx.ui.info(ctx.t('langKept', { lang: savedLocale }));
|
|
48
|
+
return ctx;
|
|
49
|
+
}
|
|
47
50
|
}
|
|
48
51
|
|
|
52
|
+
if (!ctx.interactive || ctx.options.lang) return ctx;
|
|
53
|
+
|
|
49
54
|
const options = [
|
|
50
55
|
...SUPPORTED.map((code) => ({ value: code, label: LANGUAGE_LABELS[code] })),
|
|
51
56
|
{ value: OTHER, label: OTHER_LABEL.en + ' / その他 / Ngôn ngữ khác' }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haposoft/cafekit",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.2",
|
|
4
4
|
"description": "Claude Code-first spec-driven workflow for AI coding assistants. Bundles CafeKit hapo: skills, runtime hooks, agents, and installer scaffolding.",
|
|
5
5
|
"author": "Haposoft <nghialt@haposoft.com>",
|
|
6
6
|
"license": "MIT",
|
package/src/claude/CLAUDE.md
CHANGED
|
@@ -37,7 +37,7 @@ These rules reduce common agent coding failures: hidden assumptions, overbuilt s
|
|
|
37
37
|
### 4. Goal-Driven Execution
|
|
38
38
|
|
|
39
39
|
- Convert requests into verifiable success criteria.
|
|
40
|
-
- For spec tasks, use `Completion Criteria` and
|
|
40
|
+
- For spec tasks, use `Completion Criteria` and `## Evidence` (legacy heading aliases still parse) as the source of truth.
|
|
41
41
|
- For bugs, reproduce with a failing test or concrete evidence when feasible before fixing.
|
|
42
42
|
- Loop until verification passes or a real blocker is recorded.
|
|
43
43
|
|
|
@@ -23,7 +23,7 @@ Extract and verify:
|
|
|
23
23
|
1. Declared deliverables (files, routes, entrypoints, UI surfaces, schemas, migrations)
|
|
24
24
|
2. Declared task scope (`Related Files` and direct support files that are clearly justified)
|
|
25
25
|
3. Completion Criteria
|
|
26
|
-
4. Task Evidence
|
|
26
|
+
4. Task `## Evidence` (legacy heading aliases still parse) expectations
|
|
27
27
|
5. Canonical Contracts & Invariants from the design
|
|
28
28
|
6. Named technologies and runtime choices that the task/spec explicitly requires
|
|
29
29
|
7. Runtime entrypoints/callers and reachability obligations from task evidence or the task-aware scout report
|