Always-on craft rules (TDD, KISS, DRY, YAGNI) plus a /jira-task workflow that routes work across Haiku, Sonnet and Opus.

My Claude Code toolkit. It has two parts:
/jira-task. Takes a pasted ticket from plan to reviewed commits. It routes work by cost: Haiku explores and implements test-first, Sonnet is the fallback when Haiku fails, and Opus plans and reviews.As a plugin. Use this where your Claude Code allows third-party marketplaces:
/plugin marketplace add J-h-o/jho-claude-kit
/plugin install jho-claude-kit@jho-claude-kit
As linked files. Use this where policy only allows the official marketplace, because a plugin from this marketplace would be silently turned off there. Skills and agents are symlinked into ~/.claude, so git pull updates them:
git clone https://github.com/J-h-o/jho-claude-kit.git
node jho-claude-kit/install.js
Use one method, not both. node install.js --uninstall removes exactly what the installer added. It backs up settings.json to settings.json.bak before every change. Requires Node 18 or later.
| Command | What it does | |||
|---|---|---|---|---|
| `/craft lite\ | full\ | strict\ | off` | Switch the craft level. Default is full. The level persists across sessions. |
/jira-task <pasted ticket> | Run the full ticket workflow. Paste the title, description, acceptance criteria and screenshots in the same message. | |||
/jira-task done | End the run's guard, so the main session can read and edit files itself again. |
The plan groups tasks into waves. Tasks in a wave don't share files or dependencies, and they run in parallel, each in its own git worktree, before being merged back one at a time.
<repo>-worktrees/<KEY>-task-<n>, on branches named <KEY>/task-<n>.node_modules is linked in rather than reinstalled.During a /jira-task run, a guard hook stops the main session from reading, searching, editing or browsing. Every attempt is refused with a pointer to the right subagent. This keeps tool output out of the expensive main context; subagents are never blocked.
The levels:
The rules live in rules/craft.md. Edit that one file to change them everywhere: the main session, /jira-task, and every subagent.
The active level shows as [CRAFT:FULL] and similar. The plugin pins it under the prompt by itself. The linked-files installer puts it in your statusline instead, unless you already have one; then add it to yours: node "<kit path>/hooks/craft.js" --statusline
| Agent | Model | Job |
|---|---|---|
scout | Haiku, low effort | Read-only search. Returns file:line conclusions, not file dumps. |
investigator | Sonnet, medium effort | Reproduces and debugs, including in the browser. Reports the root cause with evidence. |
implementer | Haiku, high effort | One well-specified task, test-first. Escalates to Sonnet, then Opus. |
reviewer | Opus, high effort | Reviews the diff against the acceptance criteria and the craft rules. |
node --test tests/*.test.js
claude plugin test .hooks/statusline.ts 17 lines1// Pins the craft badge as this plugin's status line, so plugin installs get it without a statusLine setting.
2// craft.js stays the one source of the badge; the linked-files install shows it through statusLine instead.
3import type { EngineInterface, Register } from 'claude-code'
4
5// Waits for the classic hooks first, so a `/craft <level>` prompt is already applied when the badge is read.
6async function refreshBadgeAfter<T>($: EngineInterface, hooksDone: Promise<T>): Promise<T> {
7 const result = await hooksDone
8 const { stdout } = await $.process.run(['node', `${$.plugin.root}/hooks/craft.js`, '--statusline'])
9 $.ui.status(stdout || undefined)
10 return result
11}
12
13export const register: Register = on => {
14 on('classic.SessionStart', ($, e, next) => refreshBadgeAfter($, next(e)))
15 on('classic.UserPromptSubmit', ($, e, next) => refreshBadgeAfter($, next(e)))
16}
17