@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.
Files changed (60) hide show
  1. package/README.md +7 -3
  2. package/bin/__tests__/installer-safety.test.js +145 -0
  3. package/bin/install.js +9 -1
  4. package/bin/lib/backup.js +60 -16
  5. package/bin/lib/context.js +1 -1
  6. package/bin/phases/claude-runtime.js +57 -4
  7. package/bin/phases/post-install.js +39 -26
  8. package/bin/phases/select-platform.js +14 -9
  9. package/package.json +1 -1
  10. package/src/claude/CLAUDE.md +1 -1
  11. package/src/claude/agents/code-auditor.md +1 -1
  12. package/src/claude/agents/debugger.md +1 -0
  13. package/src/claude/agents/god-developer.md +10 -1
  14. package/src/claude/agents/inspector.md +1 -0
  15. package/src/claude/agents/spec-maker.md +1 -1
  16. package/src/claude/agents/test-runner.md +2 -2
  17. package/src/claude/hooks/__tests__/privacy-block.test.js +50 -10
  18. package/src/claude/hooks/__tests__/spec-gate.test.js +75 -1
  19. package/src/claude/hooks/__tests__/state.test.js +65 -0
  20. package/src/claude/hooks/docs-sync.cjs +15 -21
  21. package/src/claude/hooks/inspect-block.cjs +1 -1
  22. package/src/claude/hooks/lib/config.cjs +0 -5
  23. package/src/claude/hooks/privacy-block.cjs +39 -62
  24. package/src/claude/hooks/spec-gate.cjs +18 -7
  25. package/src/claude/hooks/spec-state.cjs +4 -0
  26. package/src/claude/hooks/state.cjs +13 -3
  27. package/src/claude/hooks/usage.cjs +6 -0
  28. package/src/claude/rules/orchestrator.md +2 -2
  29. package/src/claude/rules/skill-domain-routing.md +27 -86
  30. package/src/claude/rules/skill-workflow-routing.md +11 -82
  31. package/src/claude/rules/state-sync.md +1 -1
  32. package/src/claude/runtime.json +7 -7
  33. package/src/claude/scripts/validate-spec-output.cjs +6 -0
  34. package/src/claude/skills/code-review/references/spec-compliance-review.md +1 -1
  35. package/src/claude/skills/develop/SKILL.md +26 -5
  36. package/src/claude/skills/develop/references/parallel-waves.md +112 -0
  37. package/src/claude/skills/develop/references/quality-gate.md +10 -2
  38. package/src/claude/skills/hotfix/references/parallel-patterns.md +1 -1
  39. package/src/claude/skills/inspect/SKILL.md +42 -113
  40. package/src/claude/skills/inspect/references/internal-inspection.md +16 -135
  41. package/src/claude/skills/specs/SKILL.md +134 -386
  42. package/src/claude/skills/specs/rules/tasks-generation.md +1 -1
  43. package/src/claude/skills/specs/templates/task.md +3 -1
  44. package/src/claude/skills/sync/SKILL.md +2 -2
  45. package/src/claude/skills/sync/references/sync-protocols.md +4 -4
  46. package/src/claude/skills/test/SKILL.md +1 -1
  47. package/src/claude/status.cjs +9 -6
  48. package/src/opencode/plugins/docs-sync.ts +10 -16
  49. package/src/claude/archive-command/code/SKILL.md +0 -55
  50. package/src/claude/archive-command/code/references/execution-loop.md +0 -21
  51. package/src/claude/archive-command/code.md +0 -17
  52. package/src/claude/archive-command/docs.md +0 -609
  53. package/src/claude/archive-command/review/codebase/parallel.md +0 -76
  54. package/src/claude/archive-command/spec-design.md +0 -247
  55. package/src/claude/archive-command/spec-init.md +0 -120
  56. package/src/claude/archive-command/spec-requirements.md +0 -138
  57. package/src/claude/archive-command/spec-status.md +0 -98
  58. package/src/claude/archive-command/spec-tasks.md +0 -173
  59. package/src/claude/archive-command/spec-validate.md +0 -118
  60. 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
- [![Version](https://img.shields.io/badge/version-0.13.4-blue.svg)](https://github.com/haposoft/cafekit)
5
+ [![Version](https://img.shields.io/badge/version-0.14.2-blue.svg)](https://github.com/haposoft/cafekit)
6
6
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
7
7
  [![Claude%20Code](https://img.shields.io/badge/Claude%20Code-Primary-orange.svg)](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
- ├── settings.json
86
- └── CLAUDE.md
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 = ctx.results.errors > 0 ? 1 : 0;
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 the given platform folders (relative to cwd, e.g. ['.claude']).
44
- * Only existing folders are captured. Returns the backup directory path, or
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 existing = folders.filter((f) => fs.existsSync(f));
52
- if (existing.length === 0) return null;
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
- for (const folder of existing) {
58
- copyTree(folder, path.join(backupDir, folder));
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 a snapshot back over the working tree. For each captured folder we
66
- * remove the current (possibly half-written) copy and replace it with the
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
- for (const folder of fs.readdirSync(backupDir)) {
75
- const target = folder; // backup mirrors cwd-relative folder names
76
- const source = path.join(backupDir, folder);
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
 
@@ -93,7 +93,7 @@ const PLATFORMS = {
93
93
  skillsRef: '.opencode/skills',
94
94
  commandPrefix: '/',
95
95
  sourceDir: 'claude',
96
- sourceSubdir: 'archive-command'
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 { action } = writeManagedFile({
109
- src, dest, platformFolder: PLATFORMS.claude.folder, ctx, tracker: ctx.trackers.claude
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
- let content = fs.readFileSync(claudeMdFile, 'utf8');
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
- if (markerRe.test(content)) {
73
- content = content.replace(markerRe, newSection);
74
- } else if (genericRe.test(content)) {
75
- content = content.replace(genericRe, newSection);
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: replace existing section (shared marker) or append.
136
+ // Idempotent and scoped to the installer-owned block.
121
137
  const regex = /##[^\n]*Context Overflow Indicator[^\n]*[\s\S]*?(?=\n##|\n*$)/;
122
- content = regex.test(content)
123
- ? content.replace(regex, addressingSection)
124
- : `${content.endsWith('\n') ? content : `${content}\n`}\n${addressingSection}\n`;
125
-
126
- fs.writeFileSync(claudeMdFile, content, 'utf8');
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 = fs.readFileSync(claudeMdFile, 'utf8');
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
- // Re-record post-write baselines so installer-managed files stay "pristine",
203
- // then flush each touched platform tracker.
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
- if (!ctx.interactive || ctx.options.lang) return ctx;
39
-
40
- // If already installed, restore saved locale and skip the prompt
41
- const savedLocale = getInstalledLocale();
42
- if (savedLocale) {
43
- const code = Object.keys(LANGUAGE_LABELS).find((k) => LANGUAGE_LABELS[k] === savedLocale) || 'en';
44
- ctx.setLang(code, savedLocale); // updates ctx.t to the saved language
45
- ctx.ui.info(ctx.t('langKept', { lang: savedLocale }));
46
- return ctx;
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.1",
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",
@@ -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 `Evidence` as the source of truth. Existing task files may use `Task Test Plan & Verification Evidence` or legacy `Verification & Evidence`.
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 expectations (or Task Test Plan & Verification Evidence / legacy Verification & 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, non-parallel)."
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
- - `Evidence` MUST include exact commands, artifacts/runtime surfaces, runtime reachability proof, and negative-path checks. Existing specs may use `Task Test Plan & Verification Evidence` or legacy `Verification & Evidence`.
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, Evidence, Task Test Plan & Verification Evidence, or legacy Verification & Evidence instructions, treat them as authoritative.
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 `Evidence` (or `Task Test Plan & Verification Evidence` / legacy `Verification & Evidence`) in declaration order.
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