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
2 changes: 1 addition & 1 deletion CLAUDE.md

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions docs/plans/2026-10-21-0700-feat-kaizen-upgrade-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,8 @@ release adds its signal at all.
<!-- kaizen:done -->
## Definition of done

- [ ] S1 (Kaizen 4.3.0) and S2 (4.4.0) merged.
- [ ] Every R covered by its tests above; CI green on the matrix for each slice.
- [ ] The five migrations of the Problem table detected and, where automatable, applied.
- [ ] `doctor`, `help` and `setup` point to `upgrade` when something is pending.
- [x] S1 (Kaizen 4.3.0, #44) merged; S2 (4.4.0) in review.
- [x] Every R covered by its tests above (`tests/upgrade.test.mjs`: AE1–AE5); CI green on the matrix for S1.
- [x] The five migrations of the Problem table detected and, where automatable, applied.
- [x] `doctor` reports pending migrations; `help`, `work` and `setup` relay them through the
conventions' guard rails, and `setup check` runs `upgrade`.
2 changes: 1 addition & 1 deletion plugins/kaizen/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "kaizen",
"description": "High-quality AI SDLC adapted from the Compound Engineering loop: engineering constitution enforced as gates, brainstorm, plan (traceability, threats, rollout, PR-sized slices) with independent doc review, test-first work with a Stop-hook quality gate, multi-agent code review, ship and PR watching, UI polish, learnings in docs/learnings/ read back by every cycle, ADRs, blameless postmortems, DORA metrics and releases; production commands (deploy, monitor, postmortem) in kaizen-ops",
"version": "4.3.0",
"version": "4.4.0",
"author": {
"name": "Angelo LIMA",
"email": "[email protected]"
Expand Down
13 changes: 13 additions & 0 deletions plugins/kaizen/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions: [SemVer](https://semver.org/).

## [4.4.0] - 2026-10-21

### Added
- **`kaizen doctor` reports pending migrations** (check `upgrade`): "3 Kaizen migration(s) pending
(profile-names, kaizen-ops, hook-lookup)", with how many must be done by hand, and
`node "$K" upgrade` as the fix. `help`, `work` and `setup` relay it (conventions, "Guard rails"):
they show the list, offer `upgrade --apply` before touching versioned files, and give the manual
steps; `setup check` runs `upgrade` too.
- **No breaking release without its migration.** From 4.3.0 on, a CHANGELOG release with a
**Breaking** or **Deprecated** section must register a migration with that version in
`scripts/upgrade.mjs`, or say in `NO_MIGRATION` why the repository has nothing to change; a contract
test fails otherwise.

## [4.3.0] - 2026-10-21

### Added
Expand Down
5 changes: 3 additions & 2 deletions plugins/kaizen/docs/guides/help.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ Order of the inference:
## Guard rails

`help` runs `kaizen doctor` first and tells you when the guard rails are off: the plugin not declared
for the repository, hooks not firing in this session, git hooks not wired, or a branch pushed without a
review — each with the command that fixes it.
for the repository, hooks not firing in this session, git hooks not wired, a branch pushed without a
review, or migrations a Kaizen update still asks of the repo (`kaizen upgrade` lists them, `--apply`
fixes the automatable ones) — each with the command that fixes it.

## See also

Expand Down
4 changes: 3 additions & 1 deletion plugins/kaizen/docs/guides/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ Config, detected checks, learnings, packs, constitution, gate, review state and

## Guard rails without the plugin

Setup starts with `kaizen doctor`. It offers, with your consent: `audit fix claude_settings`
Setup starts with `kaizen doctor`. After a Kaizen update, it lists what the repository still has to
change (`kaizen upgrade`: renamed profile, kaizen-ops to enable, git hooks to refresh, a folder to rename
by hand) and offers `kaizen upgrade --apply` for the automatable ones; `setup check` runs it too. It offers, with your consent: `audit fix claude_settings`
(declares `kaizen@dojo` in the versioned `.claude/settings.json`, so every clone loads Kaizen) and
`audit fix git_hooks` (git hooks that refuse an unreviewed push and a staged secret even when the plugin
is not loaded; they refuse when the Kaizen CLI cannot be found — set `git config kaizen.cli
Expand Down
11 changes: 8 additions & 3 deletions plugins/kaizen/docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ the versioned `.claude/settings.json` declares the `dojo` marketplace and enable
fresh clone or a cloud session runs without Kaizen); Claude Code hooks firing (heartbeat written by the
SessionStart hook — absent means "not verified" from a terminal, "off" with `--in-skill`, which the
entry skills pass); git hooks in `.githooks/` wired through `core.hooksPath`; verification commands
detected; the branch's review state, and whether its upstream was pushed without a review. Exit 0, or 1
with `--strict` when anything needs attention.
detected; Kaizen migrations pending after an update (`upgrade`, below); the branch's review state, and
whether its upstream was pushed without a review. Exit 0, or 1 with `--strict` when anything needs
attention.

### `upgrade [--apply] [--json]`

Expand All @@ -56,7 +57,11 @@ touches, and — when it cannot be automated — the steps to run by hand.

`--apply` runs the automatable ones that still apply, writes only what each owns (formatting and team
lines kept), and ends with the files to commit; run twice, it changes nothing. Nothing pending outside
a Kaizen repo (no `.kaizen/config.json`).
a Kaizen repo (no `.kaizen/config.json`). `kaizen doctor` reports the pending ones (check `upgrade`),
so `help`, `work` and `setup` relay them.

A release that breaks or deprecates something declares its migration here, or why it needs none
(`NO_MIGRATION` in `scripts/upgrade.mjs`): a contract test fails otherwise, from 4.3.0 on.

### `root`

Expand Down
4 changes: 4 additions & 0 deletions plugins/kaizen/references/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ before anything else:
- **production commands moved (`ops_plugin`)** → the repo deploys or watches signals with Kaizen: say the
commands are now `/kaizen-ops:deploy`, `/kaizen-ops:monitor`, `/kaizen-ops:postmortem`, and offer
`node "$K" audit fix claude_settings`, which enables `kaizen-ops@dojo` (ask first);
- **Kaizen migrations pending (`upgrade`)** → Kaizen was updated and the repo still has changes to make:
run `node "$K" upgrade`, show the list (each migration's version, why, files), offer
`node "$K" upgrade --apply` for the automatable ones (versioned files: ask first, then name the files to
commit), and relay the steps to run by hand;
- **branch pushed without a review** → `/kaizen:review` before anything else is pushed.
No ⚠ → say nothing. The doctor never blocks: it informs.

Expand Down
8 changes: 8 additions & 0 deletions plugins/kaizen/scripts/doctor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { docsRoot, git, loadConfig, verifyCommands } from './lib.mjs';
import { checkPush, currentBranch } from './review-state.mjs';
import { pending as pendingMigrations } from './upgrade.mjs';

export const MARKETPLACE = 'dojo';
export const PLUGIN_ID = `kaizen@${MARKETPLACE}`;
Expand Down Expand Up @@ -117,6 +118,13 @@ export function doctor(root, { inSkill = false, now = Date.now() } = {}) {
'/kaizen:setup',
);

// What a Kaizen update still asks of this repo, detected from its state (kaizen upgrade).
const migrations = pendingMigrations(root, config);
if (migrations.length) {
const auto = migrations.filter((m) => m.auto).length;
add('upgrade', 'Kaizen migrations', 'warn', `${migrations.length} Kaizen migration(s) pending (${migrations.map((m) => m.id).join(', ')})${auto < migrations.length ? `, ${migrations.length - auto} by hand` : ''}`, 'node "$K" upgrade (lists them), then node "$K" upgrade --apply');
} else if (existsSync(join(root, '.kaizen', 'config.json'))) add('upgrade', 'Kaizen migrations', 'ok', 'nothing to migrate');

const decl = pluginDeclared(root);
add(
'plugin_declared',
Expand Down
9 changes: 7 additions & 2 deletions plugins/kaizen/scripts/upgrade.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import { docsRoot, loadConfig, PROFILE_ALIASES } from './lib.mjs';
import { MARKETPLACE, PLUGIN_ID, gitHooksState, opsDeclared, opsNeeded } from './doctor.mjs';
import { PLUGIN_ID, gitHooksState, opsDeclared, opsNeeded } from './doctor.mjs';
import { declarePlugin, refreshHookLookups } from './githooks.mjs';

export const KAIZEN_VERSION = JSON.parse(readFileSync(fileURLToPath(new URL('../.claude-plugin/plugin.json', import.meta.url)), 'utf8')).version;
Expand Down Expand Up @@ -54,6 +54,10 @@ function declare(root) {
}
}

// Releases with a Breaking or Deprecated section that ask nothing of a repository, with the reason
// (tests/upgrade.test.mjs requires one or the other from 4.3.0 on).
export const NO_MIGRATION = {};

export const MIGRATIONS = [
{
id: 'learnings-folder',
Expand All @@ -71,7 +75,8 @@ export const MIGRATIONS = [
{
id: 'marketplace-dojo',
since: '3.1.0',
title: `the marketplace is "${MARKETPLACE}": plugins are enabled as ${PLUGIN_ID}`,
// Plain text: doctor imports this module, which must not read doctor's exports while it loads.
title: 'the marketplace is "dojo": plugins are enabled as kaizen@dojo',
applies(root) {
const old = oldMarketplaceIds(root);
const plugins = readJson(join(root, '.claude', 'settings.json'))?.enabledPlugins || {};
Expand Down
3 changes: 2 additions & 1 deletion plugins/kaizen/skills/setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ of what setup fixes.

`node "$K" doctor`, `node "$K" root`, `node "$K" config`, `node "$K" detect`, `node "$K" learnings validate`,
`node "$K" packs`, `node "$K" constitution check`, `node "$K" gate status`, `node "$K" review status`,
`node "$K" models` (warnings) → report: ✔/⚠ per point, with the proposed fix.
`node "$K" models` (warnings), `node "$K" upgrade` (migrations pending after a Kaizen update) → report:
✔/⚠ per point, with the proposed fix.
A gate left active without work in progress (`gate status` active) → propose `gate off`.

## `audit` — set up the SDLC, in order
Expand Down
41 changes: 41 additions & 0 deletions plugins/kaizen/tests/upgrade.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,44 @@ test('each migration: does not apply once fixed by hand, nor when its trigger is
assert.deepEqual(run(foreign, ['upgrade', '--json']).json.pending, []);
for (const d of [dir, ops, foreign]) cleanup(d);
});

test('AE4: doctor reports pending migrations with their count, ids and the fix', () => {
const dir = oldRepo();
const check = (d) => run(d, ['doctor', '--json']).json.checks.find((c) => c.id === 'upgrade');
const before = check(dir);
assert.equal(before.status, 'warn');
assert.equal(before.evidence, '3 Kaizen migration(s) pending (profile-names, kaizen-ops, hook-lookup)');
assert.match(before.fix, /node "\$K" upgrade/);
run(dir, ['upgrade', '--apply']);
assert.deepEqual([check(dir).status, check(dir).evidence], ['ok', 'nothing to migrate']);
// A manual one is counted as such; outside a Kaizen repo, no check at all.
const manual = tempRepo({ '.kaizen/config.json': {}, 'docs/solutions/a.md': '# a\n' });
assert.match(check(manual).evidence, /1 Kaizen migration\(s\) pending \(learnings-folder\), 1 by hand/);
const bare = tempRepo({ 'a.txt': 'a\n' });
assert.equal(check(bare), undefined);
for (const d of [dir, manual, bare]) cleanup(d);
});

// AE5: every release from 4.3.0 on that breaks or deprecates something declares its migration, or says
// why there is none — so the next one cannot fall back into a corner of the doctor.
const semver = (v) => v.split('.').map(Number);
const atLeast = (v, min) => { const [a, b] = [semver(v), semver(min)]; for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] > b[i]; return true; };
function undeclared(changelog, migrations, exempt) {
const out = [];
for (const [, version, body] of changelog.matchAll(/^## \[(\d+\.\d+\.\d+)\][^\n]*\n([\s\S]*?)(?=^## \[|(?![\s\S]))/gm)) {
if (!atLeast(version, '4.3.0') || !/^### (Breaking|Deprecated)/m.test(body)) continue;
if (!migrations.some((m) => m.since === version) && !exempt[version]) out.push(version);
}
return out;
}

test('AE5: a Breaking or Deprecated release from 4.3.0 on declares its migration or why there is none', async () => {
const { MIGRATIONS, NO_MIGRATION } = await import('../scripts/upgrade.mjs');
assert.deepEqual(undeclared(readFileSync(join(PLUGIN, 'CHANGELOG.md'), 'utf8'), MIGRATIONS, NO_MIGRATION), []);
// The check itself: a breaking 9.9.0 with nothing declared fails; a migration or an exemption lifts it;
// older releases are covered by the migrations of 4.3.0.
const fixture = '# Changelog\n\n## [9.9.0] - 2030-01-01\n\n### Breaking\n- renamed a key\n\n## [9.8.0] - 2030-01-01\n\n### Added\n- x\n\n## [4.0.0] - 2026-10-20\n\n### Breaking\n- moved\n';
assert.deepEqual(undeclared(fixture, MIGRATIONS, NO_MIGRATION), ['9.9.0']);
assert.deepEqual(undeclared(fixture, [...MIGRATIONS, { since: '9.9.0' }], NO_MIGRATION), []);
assert.deepEqual(undeclared(fixture, MIGRATIONS, { '9.9.0': 'the key is still read' }), []);
});
Loading