Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/project-changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ All notable changes to CafeKit are documented here, following

## [Unreleased]

### Fixed
- Installer no longer resets `locale.responseLanguage` to `en` on non-interactive upgrade (saved-locale restore hoisted above the interactivity check + no-downgrade guard; live regression fixture). Statusline autocompact reserve is proportional to the real context window (1M models no longer treated as 200k).

### Changed
- `spec.{scaffold_guard, completion_gate, tollgate}` documented in runtime.json; Claude reminder honors `tollgate` like OpenCode; dead `useGemini` key dropped; `usage.cjs` OAuth endpoint marked experimental; legacy `Task`-tool prose → `Agent`; `inspector`/`debugger` gain `memory: user`.

### Removed
- Legacy `archive-command/` tree (1,680 dead lines) + vestigial references.

### Added
- Self-test: settings-template ↔ migration-manifest hook consistency check (11 hooks).

## [0.14.1] - 2026-07-17

### Fixed
Expand Down
15 changes: 15 additions & 0 deletions packages/spec/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed
- **Installer preserves configured locale on non-interactive upgrade**: `selectLanguage` returned before restoring the saved locale when run with `--yes`, so every upgrade reset `locale.responseLanguage` to `en` (reproduced on 0.14.0 and 0.14.1). Saved-locale restore now runs regardless of interactivity, plus a hardening guard in `patchRuntimeLocale` never downgrades a configured label when the run made no explicit language choice. Covered by a live installer regression fixture proven to fail on the unpatched code.
- **Statusline context bar on 1M-context models**: the autocompact reserve was a hard-coded 45000 tokens (22.5% of a 200k window); it is now proportional (`0.225 × context_window_size` from the payload), so 1M windows are no longer treated as 200k. 200k behavior unchanged.

### Changed
- **`spec` toggles unified and documented**: `runtime.json` template now lists `spec.{scaffold_guard, completion_gate, tollgate}`; the Claude `spec-state.cjs` reminder honors `spec.tollgate: false` (same key the OpenCode plugin already used). Dead `skills.research.useGemini` key removed from the template and config defaults (the research skill uses native WebSearch). `paths.plans` documented.
- **`usage.cjs` marked experimental**: the OAuth usage endpoint is undocumented and may break without notice; header now states the degradation contract (`status:"unavailable"`, statusline hides the segment) and the disable switch.
- Legacy `Task`-tool prose modernized to the `Agent` tool in develop/test/hotfix skill text (deliberate backward-compat notes in `CLAUDE.md` and `subagent-patterns.md` kept). `inspector` and `debugger` agents now carry `memory: user` like `researcher`.

### Removed
- **Legacy `archive-command/` tree** (1,680 lines, 12 files): the pre-skill command-based spec workflow, superseded since the skills migration and never installed by the manifest. Vestigial `sourceSubdir` reference and its self-test assertion cleaned.

### Added
- **Self-test: settings/manifest hook consistency check** — every `hooks/*.cjs` registered in the settings template must exist in the payload and in `migration-manifest.json` `runtime.files`, and vice versa (schema-drift tripwire; 11 hooks verified).

## [0.14.1] - 2026-07-17

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion packages/spec/bin/lib/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ const PLATFORMS = {
skillsRef: '.opencode/skills',
commandPrefix: '/',
sourceDir: 'claude',
sourceSubdir: 'archive-command'
sourceSubdir: 'commands' // Dead config (manifest commands.core is []); kept for shape parity
}
// Add new platforms here:
// cursor: {
Expand Down
4 changes: 4 additions & 0 deletions packages/spec/bin/phases/post-install.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ function patchRuntimeLocale(ctx) {
// Use locale (freeform label) so custom languages propagate to the AI hook.
const locale = ctx.locale || ctx.lang;
if (data.locale.responseLanguage === locale) continue;
// Hardening: never downgrade an existing configured label to a bare default
// code when this run never made an explicit language choice (ctx.locale
// empty). Protects user config on any code path that skips selectLanguage.
if (!ctx.locale && data.locale.responseLanguage) continue;
data.locale.responseLanguage = locale;
fs.writeFileSync(rtPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
if (ctx.trackers[key]) ctx.trackers[key].record(rtPath);
Expand Down
23 changes: 14 additions & 9 deletions packages/spec/bin/phases/select-platform.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,22 @@ function getInstalledLocale() {

/** First step: pick the installer/UI language (interactive only). */
async function selectLanguage(ctx) {
if (!ctx.interactive || ctx.options.lang) return ctx;

// If already installed, restore saved locale and skip the prompt
const savedLocale = getInstalledLocale();
if (savedLocale) {
const code = Object.keys(LANGUAGE_LABELS).find((k) => LANGUAGE_LABELS[k] === savedLocale) || 'en';
ctx.setLang(code, savedLocale); // updates ctx.t to the saved language
ctx.ui.info(ctx.t('langKept', { lang: savedLocale }));
return ctx;
// Restore the saved locale BEFORE the interactivity check: a non-interactive
// upgrade (--yes) must not forget the configured language. Skipping this left
// ctx at the 'en' default, and patchRuntimeLocale then clobbered the user's
// responseLanguage on every upgrade.
if (!ctx.options.lang) {
const savedLocale = getInstalledLocale();
if (savedLocale) {
const code = Object.keys(LANGUAGE_LABELS).find((k) => LANGUAGE_LABELS[k] === savedLocale) || 'en';
ctx.setLang(code, savedLocale); // updates ctx.t to the saved language
if (ctx.interactive) ctx.ui.info(ctx.t('langKept', { lang: savedLocale }));
return ctx;
}
}

if (!ctx.interactive || ctx.options.lang) return ctx;

const options = [
...SUPPORTED.map((code) => ({ value: code, label: LANGUAGE_LABELS[code] })),
{ value: OTHER, label: OTHER_LABEL.en + ' / その他 / Ngôn ngữ khác' }
Expand Down
108 changes: 100 additions & 8 deletions packages/spec/scripts/run-skill-self-tests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -290,14 +290,6 @@ async function runStaticSemanticTests() {
assert: (content) =>
content.includes("Init is never a stop point"),
},
{
label: "legacy spec-init redirects to hapo specs resume",
file: "src/claude/archive-command/spec-init.md",
assert: (content) =>
content.includes("templates/spec-state.json") &&
content.includes("/hapo:specs resume <feature-name>") &&
!content.includes("Command block showing `/spec-requirements"),
},
{
label: "hapo:specs task rules require runtime reachability proof",
file: "src/claude/skills/specs/rules/tasks-generation.md",
Expand Down Expand Up @@ -959,6 +951,104 @@ async function runInstallerMigrationFixtureTests() {
}
}

/**
* Schema-drift tripwire: every hook command in the settings template must map
* to a real payload file that the migration manifest ships, and vice versa —
* a hook listed in runtime.files but registered nowhere is dead weight, a hook
* registered in settings but not shipped breaks at runtime.
*/
async function runSettingsManifestConsistencyCheck() {
const settings = JSON.parse(
await readFile(join(packageRoot, "src/claude/settings/settings.json"), "utf8"),
);
const manifest = JSON.parse(
await readFile(join(packageRoot, "src/claude/migration-manifest.json"), "utf8"),
);

const registered = new Set(
JSON.stringify(settings.hooks).match(/hooks\/[a-z-]+\.cjs/g) || [],
);
const shipped = new Set(
(manifest.runtime?.files || []).filter((f) => /^hooks\/[a-z-]+\.cjs$/.test(f)),
);

const failures = [];
for (const hook of registered) {
if (!shipped.has(hook)) failures.push(`registered in settings but not in manifest runtime.files: ${hook}`);
if (!(await fileExists(join(packageRoot, "src/claude", hook)))) {
failures.push(`registered in settings but payload file missing: ${hook}`);
}
}
for (const hook of shipped) {
if (!registered.has(hook)) failures.push(`shipped in manifest but registered in no settings event: ${hook}`);
}

if (failures.length > 0) {
console.error(failures.join("\n"));
console.error("[FAIL] settings/manifest hook consistency check failed");
process.exit(1);
}

console.log(`✔ settings template and manifest agree on ${registered.size} hooks`);
return 1;
}

/**
* Regression: a non-interactive upgrade (--yes/--force-overwrite) must preserve
* the configured locale.responseLanguage. Bug (0.14.0/0.14.1 era): selectLanguage
* returned before restoring the saved locale when !interactive, so
* patchRuntimeLocale clobbered the label with the 'en' default on every upgrade.
*/
async function runLocalePreservationFixtureTest() {
const root = await mkdtemp(join(tmpdir(), "cafekit-installer-locale-"));

try {
await mkdir(join(root, ".claude"), { recursive: true });

const install = (args = []) =>
spawnSync(process.execPath, [join(packageRoot, "bin", "install.js"), ...args], {
cwd: root,
input: "n\n\n",
encoding: "utf8",
env: { ...process.env, PATH: "/usr/bin:/bin" },
});

const first = install();
if (first.status !== 0) {
console.error(first.stdout, first.stderr);
console.error("[FAIL] locale fixture: fresh install failed");
process.exit(1);
}

// Simulate a configured install: user language saved as a freeform label.
const rtPath = join(root, ".claude", "runtime.json");
const rt = JSON.parse(await readFile(rtPath, "utf8"));
rt.locale = { ...(rt.locale || {}), responseLanguage: "Tiếng Việt" };
await writeFile(rtPath, `${JSON.stringify(rt, null, 2)}\n`);

// Non-interactive upgrade — the exact path that clobbered the locale.
const second = install(["--force-overwrite"]);
if (second.status !== 0) {
console.error(second.stdout, second.stderr);
console.error("[FAIL] locale fixture: upgrade run failed");
process.exit(1);
}

const after = JSON.parse(await readFile(rtPath, "utf8"));
if (after.locale?.responseLanguage !== "Tiếng Việt") {
console.error(
`[FAIL] locale fixture: responseLanguage became ${JSON.stringify(after.locale?.responseLanguage)} after upgrade (expected "Tiếng Việt")`,
);
process.exit(1);
}

console.log("✔ installer upgrade preserves configured locale.responseLanguage");
return 1;
} finally {
await rm(root, { recursive: true, force: true });
}
}

async function runOpenCodeInstallerFixtureTests() {
const root = await mkdtemp(join(tmpdir(), "cafekit-opencode-installer-"));

Expand Down Expand Up @@ -1520,7 +1610,9 @@ async function main() {
console.log("\n[skill-test] skill catalog checks");
totalTests += runSkillCatalogTests();
console.log("\n[skill-test] installer migration fixtures");
totalTests += await runSettingsManifestConsistencyCheck();
totalTests += await runInstallerMigrationFixtureTests();
totalTests += await runLocalePreservationFixtureTest();
console.log("\n[skill-test] OpenCode installer fixtures");
totalTests += await runOpenCodeInstallerFixtureTests();
console.log("\n[skill-test] spec artifact validator fixtures");
Expand Down
1 change: 1 addition & 0 deletions packages/spec/src/claude/agents/debugger.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
name: debugger
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."
model: sonnet
memory: user
tools: Glob, Grep, Read, Bash, WebFetch, WebSearch
---

Expand Down
1 change: 1 addition & 0 deletions packages/spec/src/claude/agents/inspector.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ name: inspector
tools: Glob, Grep, Read, Bash
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."
model: haiku
memory: user
---

# Inspect — Codebase Scout
Expand Down
17 changes: 0 additions & 17 deletions packages/spec/src/claude/archive-command/code.md

This file was deleted.

55 changes: 0 additions & 55 deletions packages/spec/src/claude/archive-command/code/SKILL.md

This file was deleted.

This file was deleted.

Loading