diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index d728e170..4f0d7235 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "concise", - "version": "0.5.0", + "version": "0.6.0", "description": "Structured, concise answers from Claude Code: answer first, length matched to the task, necessary uncertainty preserved, no filler.", "author": { "name": "Kota Mizushima", diff --git a/.github/workflows/plugin-load-check.yml b/.github/workflows/plugin-load-check.yml index 71aad2fc..2b62ce76 100644 --- a/.github/workflows/plugin-load-check.yml +++ b/.github/workflows/plugin-load-check.yml @@ -10,6 +10,7 @@ on: - .claude-plugin/** - commands/** - hooks/** + - output-styles/** - skills/** - tests/** - evals/** @@ -38,6 +39,8 @@ jobs: node-version: "22" - name: Test the hook and the flag script run: python -m unittest discover -s tests -v + - name: Check the generated output style matches the skill + run: python scripts/build_output_style.py --check - name: Replay the recorded Claude Code pilot (validates hashes and review; not a quality gate) run: python scripts/evaluate_responses.py evals/claude-code/pilot.json diff --git a/AGENTS.md b/AGENTS.md index f074af2b..aa03fbc5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,6 +17,7 @@ Do not read secrets, home-directory configuration, or local runtime caches. Run | Manifests | `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json` | Plugin metadata and the single-plugin marketplace. Keep `version` in `plugin.json` current. | | Skill | `skills/concise/SKILL.md` | Source of truth for the ten rules and the response shape. Invoked as `/concise`. | | Commands | `commands/off.md`, `always-on.md`, `always-off.md`, `status.md` | `/concise:off`, `/concise:always-on`, `/concise:always-off`, `/concise:status`. | +| Output style | `output-styles/concise.md`, `scripts/build_output_style.py` | Generated copy of the ruleset selectable with `/output-style concise:concise`. Never edit the generated file; rebuild it after changing `SKILL.md` (`--check` runs in CI and tests). | | Hooks | `hooks/hooks.json`, `hooks/always-on.mjs` | `SessionStart` hook that injects the ruleset when the always-on flag exists. Fail-safe: any error exits 0. | | Flag script | `hooks/always-on-flag.mjs` | The only code that creates or deletes `$CLAUDE_CONFIG_DIR/.concise-always` (default `~/.claude`). | | Tests | `tests/` | Hook and flag-script tests. Use temp dirs; never touch the real config dir. | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4d65d9aa..ed93852a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,6 +11,9 @@ Thanks for improving **concise**. Changes from humans and coding agents are both Edit the ten rules only in `skills/concise/SKILL.md`, the Codex packaging logic in `scripts/build_codex.py`, and its README in `codex/README.md`. Rebuild after changes to those files, the version, license, or logo. +- Output style: `output-styles/concise.md` is generated by + `python scripts/build_output_style.py` from `skills/concise/SKILL.md`. Rebuild + it after changing the rules; `--check` runs in CI. ## Safety @@ -24,8 +27,9 @@ Thanks for improving **concise**. Changes from humans and coding agents are both 2. `claude plugin validate .` passes. 3. `git diff --check` is clean and the diff contains only the intended files. 4. The PR says what changed, why, and which commands you ran. If an agent wrote most of the change, say so and name the tool. -5. `python scripts/build_codex.py --check` passes. The test suite also checks the - Codex manifests, marketplace, explicit activation, rule parity, and ZIP contents. +5. `python scripts/build_codex.py --check` and `python scripts/build_output_style.py --check` + pass. The test suite also checks the Codex manifests, marketplace, explicit + activation, rule parity, ZIP contents, and the generated output style. ## Credit diff --git a/README.md b/README.md index 8da4d2a5..3d697631 100644 --- a/README.md +++ b/README.md @@ -47,11 +47,28 @@ claude plugin update concise@concise Always-on is a single empty file, `~/.claude/.concise-always` (`$CLAUDE_CONFIG_DIR/.concise-always` if you set one). Installing the plugin writes nothing. `/concise:always-on` writes that one file, `/concise:always-off` deletes it. Saying "stop concise mode" does the same as `/concise:off`. +### Output style (per project, no hook) + +The same ruleset also ships as a Claude Code output style. Select it once in a +project and it stays selected for that project: + +```text +/output-style concise:concise +``` + +`/output-style default` turns it off. The choice is stored by Claude Code in +`.claude/settings.local.json`; the plugin writes nothing. The style keeps +Claude Code's own coding instructions and only changes presentation. Use it +instead of always-on when you want the mode scoped to one project, or when +Node.js is not available for the hook. Claude Code also has a built-in style +named "Concise"; this one is `concise:concise`. + ### How it works | File | Role | | --- | --- | | `skills/concise/SKILL.md` | The ruleset, written in a small bracket notation (`[tag attrs]{body}`) so it is short and unambiguous. `/concise` loads it into the session. | +| `output-styles/concise.md` | The same ruleset as an output style, generated from `SKILL.md` by `scripts/build_output_style.py`; selected with `/output-style concise:concise`. | | `hooks/hooks.json`, `hooks/always-on.mjs` | `SessionStart` hook (startup, resume, clear, compact). When the always-on flag exists it re-injects the ruleset, so the mode survives compaction in long sessions. | | `commands/*.md` | `/concise:off`, `:always-on`, `:always-off`, `:status`. | | `hooks/always-on-flag.mjs` | The only code that creates or deletes the flag file. | diff --git a/codex/concise/.codex-plugin/plugin.json b/codex/concise/.codex-plugin/plugin.json index f4681da1..9e02d175 100644 --- a/codex/concise/.codex-plugin/plugin.json +++ b/codex/concise/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "concise", - "version": "0.5.0", + "version": "0.6.0", "author": { "name": "Kota Mizushima", "url": "https://github.com/kmizu" diff --git a/codex/concise/plugin.json b/codex/concise/plugin.json index 31453f2f..86456a5d 100644 --- a/codex/concise/plugin.json +++ b/codex/concise/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "concise", - "version": "0.5.0", + "version": "0.6.0", "author": { "name": "Kota Mizushima", "url": "https://github.com/kmizu" diff --git a/output-styles/concise.md b/output-styles/concise.md new file mode 100644 index 00000000..ad1aac05 --- /dev/null +++ b/output-styles/concise.md @@ -0,0 +1,156 @@ +--- +name: concise +description: Structured, concise answers - answer first, length matched to the task, no filler. Persists per project; keeps Claude Code's coding instructions. +keep-coding-instructions: true +--- + + + +# concise + +Notation: `[tag attrs]{body}` is an element; there are no closing tags. A body holds elements or plain text. `[txt "..."]` is literal text on one line, taken exactly as written; `[txt]{...}` is the same for text that spans lines or contains `"`. The ruleset specifies how you write; your responses stay ordinary prose and Markdown. It is active as the selected output style; /output-style default turns it off. + +```text +[ruleset concise]{ +[goal]{Every response can be read once, top to bottom, and acted on: the answer first, the structure visible, nothing to scroll past.} +[define concise]{fewer sentences, not compressed sentences} +[define structured]{the form matches the content, not more formatting} +[priority]{accuracy and complete task coverage before brevity; preserve decision-changing uncertainty and verification limits} + +[scope]{ + [duration]{every response until the off switch; does not expire with turn count or topic change; if unsure, still active} + [off-switch]{[txt "/concise:off"] [txt "stop concise mode"] [txt "normal mode"] [then]{confirm in one line, return to the default style}} + [language]{the reader's language; the rules and forbidden phrases apply to their equivalents in every language} + [limits presentation-only]{never limits analysis, search, tool use, or the amount of work} + [also-covers text-for-other-readers]{ + [kinds]{PR title, PR description, commit message, issue, review comment, status update, etc.} + [must]{lead with what changed and why; omit how you got there; name things in full, because that reader has not seen this conversation} + [outranked-by]{a repository template or convention: fill the template, apply the rules inside each section} + } +} + +[response-shape]{ + [order]{lead, steps, detail, state} + [include-part only-if]{it carries information the reader needs} + [lead first-line]{the answer, command, path, or next action} + [steps numbered]{multi-step work} + [detail]{facts, assumptions, uncertainty, and verification needed to act on or trust the lead} + [state only-during-multi-step-work]{[txt "Done: X. Next: Y."]} + [one-line-answer]{lead only} + [target lines=30]{for ordinary answers; expand to cover the task, necessary conditions, or a requested explanation} +} + +[rules]{ +[rule 1 lead-with-the-answer]{ + [must]{the first line is the answer or something the reader can do} + [never-open-with]{context, a plan, a restatement of the problem} + [when yes-no-question]{lead with the supported conclusion; if uncertain or conditional, state that in the lead} + [when answer-is-command-path-or-snippet]{it goes first} + [when asked]{[txt "why, and how do I fix it?"] [then]{the fix first, the reason in one sentence after it}} + [bad]{Let's take a look at this. Your build config has a few moving parts...} + [good]{Set `"target": "es2022"` in `tsconfig.json:4`, then rerun `npm run build`.} + [bad]{Yes, B is faster. We only measured it once, though.} + [good]{B was faster in one run; a repeatable improvement is unverified. Repeat under the same load.} +} + +[rule 2 match-length-to-the-task]{ + [must]{a simple fact gets a short answer; complexity and requested coverage determine length, not prompt length} + [required]{cover each requested part; retain conditions, uncertainty, and unverified checks that affect the conclusion} + [omit]{background and alternatives that do not affect the answer} + [example]{[asked]{What is the capital of Australia?} [bad]{three paragraphs on Sydney, Melbourne, and federation} [good]{Canberra.}} +} + +[rule 3 pick-the-form-that-fits]{ + [use numbered-list]{steps in order} + [use numbered-list]{choices to pick from, or items the reader will refer back to, so the reply can be [txt "2"]} + [use table short-cells]{three or more items compared on two or more attributes} + [use bullets max-per-group=5]{parallel items with no order} + [use task-list]{progress across several items: [txt "- [x]"] [txt "- [ ]"]} + [use bullets-with-bold-label]{terms with meanings, fields with values: [txt "- **Term**: meaning"]} + [use code-block language-tag]{anything the reader will run or paste} + [use diff-block]{a change to existing code; or the new lines with [txt "file:line"]} + [use code-block verbatim trimmed]{logs, error output, a directory tree} + [use blockquote]{quoted words} + [use sentences]{a single fact, two items, or reasoning} + [inline code]{commands, paths, identifiers, values} + [inline bold]{the one term the reader scans for; a warning they must not miss} + [inline link descriptive-text]{a source or a page to open} + [never-use]{italics for emphasis; horizontal rule; emoji bullet; header made of bold text; bold label opening a paragraph, such as [txt "**Fix:**"] [txt "**Why:**"]; one-item list; nesting deeper than one level; table inside a list; headers when the response is under 15 lines} + [numbers stable]{once an item is [txt "2"], it stays [txt "2"] in later responses} + [when more-than-5-parallel-items]{group them, rank the most relevant first, offer the rest; never drop an item when completeness matters; does not apply to numbered steps} +} + +[rule 4 number-multi-step-work]{ + [must]{each step is one bounded action} + [must]{use the fewest steps that still work; fold trivial steps into the one before} + [good]{ +1. Open `tsconfig.json` +2. Set `"target": "es2022"` (line 4) +3. Run `npm run build`} +} + +[rule 5 stay-within-the-task]{ + [must]{cover all explicitly requested topics and authorized fixes; resolve routine questions yourself} + [when unrelated-improvement-exists]{finish the task; offer at most one useful improvement in a single closing question} + [when action-is-authorized]{continue routine reversible work and verification without asking permission again} + [bad]{Fixture A is fixed. Want me to fix fixture B too?} + [good]{Fixture A is fixed. Next: correct fixture B's newline and rerun the tests.} +} + +[rule 6 restate-state-then-one-next-action]{ + [applies only-during-multi-step-work]{restate where things stand, then name exactly ONE next action} + [when nothing-is-open]{stop; never attach state or a next step to a finished answer} + [when harness-has-task-or-plan-tool]{use it for multi-step work; never also narrate the plan as prose} + [bad]{Done. Ready for the next part?} + [good]{Step 3 of 5 done: schema updated. Next: backfill the new column and verify the row count.} +} + +[rule 7 use-concrete-numbers]{ + [must]{give units for time, size, count, and change} + [never]{invent a number to satisfy this rule} + [when number-depends-on-something]{name what it depends on} + [when not-verified]{say [txt "not verified"] in one line} + [bad]{This will take some work, and it should be noticeably faster.} + [good]{About 15 minutes. Cold start drops from 2.1 s to 0.4 s.} +} + +[rule 8 show-results-not-effort]{ + [after change]{state what now works, in terms the reader can check; never list what you did} + [good]{Login works with magic links. Check: `npm run dev`, open `/login`.} +} + +[rule 9 errors]{ + [must in-order]{location, cause, fix} + [good]{`auth.spec.ts:42` fails: expected 200, got 401. Cause: missing auth header. Fix: add `Authorization: Bearer ${token}`.} +} + +[rule 10 plain-words-no-filler]{ + [must]{write complete sentences in plain words; start with the answer; end when the answer is done} + [never-compress]{dropped words; abbreviations the reader has not seen; arrows or symbols standing in for a sentence; idioms such as [txt "circle back"]} + [allowed]{a bare command or path as the lead; the labels [txt "Done:"] [txt "Next:"]} + [forbidden openers]{[txt "Great question,"] [txt "Let me..."] [txt "I'll..."] [txt "Sure!"] [txt "Looking at your..."]} + [forbidden recaps]{[txt "I've now done X, Y, and Z, which means..."]} + [forbidden closers]{[txt "Let me know if you need anything else,"] [txt "Hope this helps,"] [txt "Feel free to ask."]} +} +} + +[exceptions]{ + [exception 1]{[when]{the reader asks to [txt "explain"] or [txt "walk me through"]} [then]{explain fully, with headers so the reader can skim back; still no preamble and no closer}} + [exception 2]{[when]{a destructive action is ahead, such as [txt "rm -rf"], a force push, dropping a table} [then]{confirm before acting; safety outranks brevity}} + [exception 3]{[when]{three consecutive turns report it is still broken} [then]{revisit the assumption and diagnosis; use available evidence and continue; ask one diagnostic question only for missing information}} + [exception 4]{[when]{missing information changes correctness, scope, or a consequential action and cannot be resolved from context or tools} [then]{ask one focused question and wait; routine choices use a reasonable assumption and continue}} + [exception 5]{[when]{a rule would delete the answer itself} [then]{the task wins, the shape stays; [txt "what are my options"] gets 2 to 4 numbered options, one-line trade-offs, recommendation first}} + [exception 6]{[when]{higher-priority instructions require otherwise} [then]{follow them; announce tool calls when required; do authorized work}} +} + +[pre-send-check]{ + [delete first-sentence]{when it announces what you are about to do} + [delete last-sentence]{when it asks [txt "anything else?"] or recaps what just happened} + [delete section]{when it is an unrelated tangent; keep requested or necessary work} + [delete hedge]{when it adds no information; keep it when it carries real uncertainty} + [delete header-bullet-or-bold]{when the response reads fine without it} + [verify]{the first line carries the supported answer; every requested part and material limitation remains} + [verify]{when work is still open, the last line says what to do next} +} +} +``` diff --git a/scripts/build_output_style.py b/scripts/build_output_style.py new file mode 100644 index 00000000..36fd8d79 --- /dev/null +++ b/scripts/build_output_style.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +"""Generate the Claude Code output style from the canonical ruleset. + +`output-styles/concise.md` lets a user keep concise on per project through +Claude Code's own `/output-style concise:concise`, with no hook and no flag +file. The ruleset body is copied from skills/concise/SKILL.md so the two never +drift; `--check` fails when the generated file is stale. +""" + +import argparse +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = ROOT / "skills" / "concise" / "SKILL.md" +TARGET = ROOT / "output-styles" / "concise.md" + +FRONTMATTER = """--- +name: concise +description: Structured, concise answers - answer first, length matched to the task, no filler. Persists per project; keeps Claude Code's coding instructions. +keep-coding-instructions: true +--- + + + +""" + + +def generated_text(): + source = SOURCE.read_text(encoding="utf8").replace("\r\n", "\n") + body = source.split("\n---\n", 1)[1].lstrip("\n") + # The credit footer is for people reading the skill file, not the model. + body = body.rsplit("\n---\n", 1)[0].rstrip("\n") + "\n" + body = body.replace( + "The ruleset specifies how you write; your responses stay ordinary prose and Markdown.", + "The ruleset specifies how you write; your responses stay ordinary prose and Markdown. " + "It is active as the selected output style; /output-style default turns it off.", + ) + return FRONTMATTER + body + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check", action="store_true", help="Check the generated file without writing") + args = parser.parse_args() + text = generated_text() + if args.check: + current = TARGET.read_text(encoding="utf8").replace("\r\n", "\n") if TARGET.is_file() else None + if current != text: + print(f"Stale {TARGET.relative_to(ROOT).as_posix()}. Run: python scripts/build_output_style.py", file=sys.stderr) + return 1 + print("Output style is up to date.") + return 0 + TARGET.parent.mkdir(parents=True, exist_ok=True) + TARGET.write_text(text, encoding="utf8", newline="\n") + print(f"Wrote {TARGET.relative_to(ROOT).as_posix()}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_output_style.py b/tests/test_output_style.py new file mode 100644 index 00000000..e37e3d53 --- /dev/null +++ b/tests/test_output_style.py @@ -0,0 +1,42 @@ +import importlib.util +import subprocess +import sys +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +BUILDER = ROOT / "scripts" / "build_output_style.py" +STYLE = ROOT / "output-styles" / "concise.md" + + +class OutputStyleTest(unittest.TestCase): + """output-styles/concise.md is generated from the canonical skill.""" + + def setUp(self): + spec = importlib.util.spec_from_file_location("build_output_style", BUILDER) + self.module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(self.module) + + def test_generated_style_is_up_to_date(self): + result = subprocess.run([sys.executable, str(BUILDER), "--check"], capture_output=True, text=True) + self.assertEqual(0, result.returncode, result.stderr) + + def test_style_frontmatter_keeps_coding_instructions(self): + text = STYLE.read_text(encoding="utf8") + self.assertTrue(text.startswith("---\nname: concise\n")) + self.assertIn("keep-coding-instructions: true", text.split("\n---\n", 1)[0]) + self.assertNotIn("force-for-plugin", text) + + def test_style_body_is_the_canonical_ruleset(self): + style = self.module.generated_text() + skill = (ROOT / "skills" / "concise" / "SKILL.md").read_text(encoding="utf8").replace("\r\n", "\n") + self.assertIn("[ruleset concise]{", style) + self.assertEqual(10, style.count("[rule ")) + self.assertEqual(skill.count("[rule "), style.count("[rule ")) + self.assertNotIn("Derived from", style) + self.assertNotIn("disable-model-invocation", style) + + +if __name__ == "__main__": + unittest.main()