@haposoft/cafekit 0.14.1 → 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 +7 -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/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/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/rules/orchestrator.md +2 -2
- package/src/claude/rules/skill-domain-routing.md +27 -86
- package/src/claude/rules/skill-workflow-routing.md +11 -82
- 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/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
|
|
|
@@ -82,10 +82,14 @@ Claude Code targets:
|
|
|
82
82
|
├── cafekit.json
|
|
83
83
|
├── status.cjs
|
|
84
84
|
├── runtime.json
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
└── settings.json
|
|
86
|
+
|
|
87
|
+
CLAUDE.md
|
|
87
88
|
```
|
|
88
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
|
+
|
|
89
93
|
OpenCode targets:
|
|
90
94
|
|
|
91
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
|
}
|
|
@@ -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
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
name: debugger
|
|
3
3
|
description: "Investigates bugs, incidents, CI/log/DB/performance/frontend failures, traces exact root causes with evidence, and hands off a verification-ready fix plan. Edits code only when explicitly requested by a fix workflow."
|
|
4
4
|
model: sonnet
|
|
5
|
+
memory: user
|
|
5
6
|
tools: Glob, Grep, Read, Bash, WebFetch, WebSearch
|
|
6
7
|
---
|
|
7
8
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: god-developer
|
|
3
|
-
description: "Primary code execution agent. Receives specifications (spec) from hapo:specs or task files and transforms them into production-grade source code. Operates on a Single-Track principle (linear
|
|
3
|
+
description: "Primary code execution agent. Receives specifications (spec) from hapo:specs or task files and transforms them into production-grade source code. Operates on a Single-Track principle within its workspace (linear inside one working tree; multiple instances may run concurrently in separate isolated worktrees)."
|
|
4
4
|
model: sonnet
|
|
5
5
|
tools: Glob, Grep, Read, Edit, Write, NotebookEdit, Bash, WebFetch, WebSearch
|
|
6
6
|
---
|
|
@@ -94,3 +94,12 @@ Upon completion, output a concise report in this format:
|
|
|
94
94
|
- Do not add AI attribution to code or commit messages.
|
|
95
95
|
- Prioritize security (validate input, never hardcode secrets).
|
|
96
96
|
- Code should be self-documenting — only add comments for complex logic.
|
|
97
|
+
|
|
98
|
+
## Worktree Conduct (Parallel Wave dispatch)
|
|
99
|
+
|
|
100
|
+
When dispatched into an isolated git worktree by `hapo:develop --parallel`:
|
|
101
|
+
|
|
102
|
+
- Work ONLY inside this worktree; Single-Track discipline applies unchanged within it.
|
|
103
|
+
- Do NOT edit `spec.json` or any `tasks/*.md` — the orchestrator is the single writer of spec state.
|
|
104
|
+
- Do NOT touch files outside this task's `Related Files`.
|
|
105
|
+
- Commit your completed work: `git commit -m "task(<id>): <title>"` — an uncommitted worktree cannot be merged back.
|
|
@@ -3,6 +3,7 @@ name: inspector
|
|
|
3
3
|
tools: Glob, Grep, Read, Bash
|
|
4
4
|
description: "Codebase structure scanner. Use this agent when you need to quickly scout/inspect the codebase architecture, files, and directories. Specializes in finding relevant files for a given work scope before implementation begins."
|
|
5
5
|
model: haiku
|
|
6
|
+
memory: user
|
|
6
7
|
---
|
|
7
8
|
|
|
8
9
|
# Inspect — Codebase Scout
|
|
@@ -127,7 +127,7 @@ Before writing `design.md`, select a discovery mode and record the reason:
|
|
|
127
127
|
- When requirement coverage format: list numeric IDs only, no descriptive suffixes
|
|
128
128
|
- Apply `(P)` parallel markers when applicable (load `.claude/skills/specs/rules/tasks-parallel-analysis.md`)
|
|
129
129
|
- Every task MUST use the compact implementation-ready shape: `Context`, `Steps`, `Requirements`, `Related Files`, `Completion Criteria`, `Evidence`, `Risk Assessment`.
|
|
130
|
-
-
|
|
130
|
+
- `## Evidence` (legacy heading aliases still parse) MUST include exact commands, artifacts/runtime surfaces, runtime reachability proof, and negative-path checks.
|
|
131
131
|
- Completion criteria MUST be objective enough that a downstream quality gate can prove them without guesswork.
|
|
132
132
|
- UI/app/runtime workflows MUST include a final integration/reachability task or final integration section that names the real entrypoint and proves all scoped user-facing surfaces are wired.
|
|
133
133
|
- Do not allow orphan task outputs: components, services, hooks, routes, commands, workers, providers, reducers, data loaders, and generated artifacts must be reachable now or assigned to a named later integration task.
|
|
@@ -11,7 +11,7 @@ You are a battle-hardened QA engineer who has been burned by production incident
|
|
|
11
11
|
|
|
12
12
|
## Task-Aware Inputs
|
|
13
13
|
|
|
14
|
-
If the prompt includes task file paths, Completion Criteria,
|
|
14
|
+
If the prompt includes task file paths, Completion Criteria, or `## Evidence` (legacy heading aliases still parse) instructions, treat them as authoritative.
|
|
15
15
|
Diff-aware test selection does NOT replace task-specific verification.
|
|
16
16
|
If the task/spec names a specific framework, auth system, transport, or shared-state boundary, keep that contract visible while evaluating evidence.
|
|
17
17
|
If the prompt includes a feature name or `specs/<feature>`, load `spec.json`, `requirements.md`, `design.md`, and the active/recent task files. Treat `scope_lock`, Completion Criteria, and Evidence as the test contract.
|
|
@@ -30,7 +30,7 @@ Select tests by the task's touched surface:
|
|
|
30
30
|
## Command Resolution Order
|
|
31
31
|
|
|
32
32
|
When the task file names exact commands, use this order:
|
|
33
|
-
1. Run every exact executable command from
|
|
33
|
+
1. Run every exact executable command from `## Evidence` (legacy heading aliases still parse) in declaration order.
|
|
34
34
|
2. Run repo-default typecheck/test/build commands only to fill gaps not already covered above.
|
|
35
35
|
3. Apply diff-aware test selection only after task-mandated commands are satisfied.
|
|
36
36
|
|