From 48c10161e7d19cac8ff649ff23225d5158452733 Mon Sep 17 00:00:00 2001 From: Hayden Luckenbach Date: Tue, 15 Sep 2026 22:04:56 -0500 Subject: [PATCH 1/2] v1.1.0 --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- .cursor/skills/blunt/SKILL.md | 2 +- CHANGELOG.md | 12 +++++++ extensions/blunt.ts | 65 ++++++++++++++++++++++++++++------- gemini-extension.json | 2 +- kimi.plugin.json | 2 +- package.json | 2 +- qwen-extension.json | 2 +- skills/blunt/SKILL.md | 2 +- 10 files changed, 72 insertions(+), 21 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 1e971f3..a4d6e6c 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "blunt", - "version": "1.0.0", + "version": "1.1.0", "description": "Shape output for an ADHD reader, strip AI tells, and write plain Simplified Technical English: lead with the next action, number steps, suppress tangents, restate state, make wins visible.", "author": { "name": "Hayden Luckenbach", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 855f603..375c70f 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "blunt", - "version": "1.0.0", + "version": "1.1.0", "description": "Blunt output for Codex: action first, numbered steps, no AI tells, plain Simplified Technical English, and visible progress.", "author": { "name": "Hayden Luckenbach", diff --git a/.cursor/skills/blunt/SKILL.md b/.cursor/skills/blunt/SKILL.md index 77d6560..541bf57 100644 --- a/.cursor/skills/blunt/SKILL.md +++ b/.cursor/skills/blunt/SKILL.md @@ -12,7 +12,7 @@ description: >- disable-model-invocation: true license: MIT metadata: - version: "1.0.0" + version: "1.1.0" category: "productivity" tags: "ADHD, output style, plain English, Simplified Technical English, AI tells" --- diff --git a/CHANGELOG.md b/CHANGELOG.md index 310b7e2..d00fd51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to blunt are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow [SemVer](https://semver.org). +## [1.1.0] - 2026-09-16 + +### Added + +- Persisted `/blunt` preference: every toggle (`/blunt`, `/blunt on|off`, `stop blunt mode`) writes the choice to the agent config, so future OMP and Pi sessions start in the same mode without re-running the command. A session's own history and `--blunt` still win for the current run. + +### Changed + +- `stop blunt mode` and `normal mode` now persist the OFF choice too: earlier 1.0.0 sessions forgot the mode between sessions; 1.1.0 remembers it until you run `/blunt` again. +- `/blunt` command description and mode notifications now state the persisted behavior. + ## [1.0.0] - 2026-09-09 ### Added @@ -13,4 +24,5 @@ All notable changes to blunt are documented here. The format follows [Keep a Cha - Test harness: package consistency validator, cross-runtime unit tests, extension RPC smoke tests with no model calls, paired baseline-vs-candidate evals with a blind judge and release gate, and a deterministic style linter. - GitHub Actions: `validate.yml` on every push and pull request, `evals.yml` on manual dispatch. +[1.1.0]: https://github.com/hayfever/blunt/releases/tag/v1.1.0 [1.0.0]: https://github.com/hayfever/blunt/releases/tag/v1.0.0 \ No newline at end of file diff --git a/extensions/blunt.ts b/extensions/blunt.ts index 90f8363..49579b6 100644 --- a/extensions/blunt.ts +++ b/extensions/blunt.ts @@ -1,4 +1,4 @@ -import { existsSync, readFileSync } from "node:fs"; +import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { @@ -29,7 +29,7 @@ const STOP_PHRASES: Record = { "normal mode": true, }; const RULES_HEADER = - 'BLUNT MODE ACTIVE. The ruleset below applies to every response until turned off. "stop blunt mode" or "normal mode" turns it off for this session.'; + 'BLUNT MODE ACTIVE. The ruleset below applies to every response until turned off. "stop blunt mode" or "normal mode" turns it off for this session and future sessions. Run /blunt to switch it back on.'; const DISABLED_NOTICE = "BLUNT MODE OFF. Ignore the blunt ruleset injected earlier in this conversation and return to your default response style."; @@ -40,18 +40,40 @@ type BluntModeState = { type BluntConfig = { alwaysOn?: boolean; hideStatus?: boolean; + /** + * Persisted toggle state. /blunt and the stop phrases write it so the + * choice carries to future sessions; it outranks the legacy always-on + * defaults but not a session's own saved state. + */ + enabled?: boolean; }; function loadConfig(): BluntConfig { try { - return JSON.parse( - readFileSync(join(getAgentDir(), "blunt.json"), "utf8"), - ); + return JSON.parse(readFileSync(join(getAgentDir(), "blunt.json"), "utf8")); } catch { return {}; } } +/** + * Write the toggle choice so future sessions start in the same mode. + * Returns false when the preference file cannot be written. + */ +function persistState(nextEnabled: boolean): boolean { + const merged: BluntConfig = { ...loadConfig(), enabled: nextEnabled }; + try { + writeFileSync( + join(getAgentDir(), "blunt.json"), + `${JSON.stringify(merged, null, 2)}\n`, + "utf8", + ); + return true; + } catch { + return false; + } +} + function stripFrontmatter(content: string): string { return content .replace( @@ -163,12 +185,18 @@ export default function bluntExtension(pi: ExtensionAPI) { const restoreState = (ctx: ExtensionContext): void => { const savedState = getSavedState(ctx); - const enabledByDefault = - pi.getFlag("blunt") === true || - config.alwaysOn === true || - existsSync(alwaysOnFlag); - - enabled = savedState ?? enabledByDefault; + const cliFlag = pi.getFlag("blunt") === true; + const persisted = loadConfig().enabled; + const enabledByDefault = config.alwaysOn === true || existsSync(alwaysOnFlag); + + // A session's own history wins, then the persisted toggle, then the + // launch-time defaults. --blunt keeps force-on semantics for that run. + const defaultForSession = cliFlag + ? true + : typeof persisted === "boolean" + ? persisted + : enabledByDefault; + enabled = savedState ?? defaultForSession; updateStatus(ctx); syncContext(ctx); }; @@ -176,9 +204,20 @@ export default function bluntExtension(pi: ExtensionAPI) { const setEnabled = (nextEnabled: boolean, ctx: ExtensionContext): void => { enabled = nextEnabled; pi.appendEntry(STATE_ENTRY_TYPE, { enabled } satisfies BluntModeState); + + if (!persistState(nextEnabled)) { + ctx.ui.notify( + "Could not save the blunt preference; this toggle is session-only.", + "warning", + ); + } + updateStatus(ctx); syncContext(ctx); - ctx.ui.notify(`Blunt mode ${enabled ? "enabled" : "disabled"}`, "info"); + ctx.ui.notify( + `Blunt mode ${enabled ? "enabled" : "disabled"} (saved for future sessions)`, + "info", + ); }; pi.registerFlag("blunt", { @@ -188,7 +227,7 @@ export default function bluntExtension(pi: ExtensionAPI) { }); pi.registerCommand("blunt", { - description: "Toggle blunt output for this session", + description: "Toggle blunt output (persists across sessions)", handler: async (args, ctx) => { const argument = args.trim().toLowerCase(); diff --git a/gemini-extension.json b/gemini-extension.json index f4fd983..21df686 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,6 +1,6 @@ { "name": "blunt", - "version": "1.0.0", + "version": "1.1.0", "description": "Shape Gemini CLI output for an ADHD reader, strip AI tells, and write plain Simplified Technical English: lead with the next action, number steps, suppress tangents, restate state, make wins visible.", "contextFileName": "GEMINI.md" } \ No newline at end of file diff --git a/kimi.plugin.json b/kimi.plugin.json index a4316a9..a008c18 100644 --- a/kimi.plugin.json +++ b/kimi.plugin.json @@ -1,6 +1,6 @@ { "name": "blunt", - "version": "1.0.0", + "version": "1.1.0", "description": "One output style: lead with the next action (ADHD-friendly), strip AI tells from prose, write technical text in plain Simplified Technical English.", "license": "MIT", "homepage": "https://github.com/hayfever/blunt", diff --git a/package.json b/package.json index b754259..0aa266b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blunt", - "version": "1.0.0", + "version": "1.1.0", "description": "One output style: lead with the next action (ADHD-friendly), strip AI tells from prose, write technical text in plain Simplified Technical English.", "license": "MIT", "homepage": "https://github.com/hayfever/blunt", diff --git a/qwen-extension.json b/qwen-extension.json index 182d932..cf6dc23 100644 --- a/qwen-extension.json +++ b/qwen-extension.json @@ -1,6 +1,6 @@ { "name": "blunt", - "version": "1.0.0", + "version": "1.1.0", "description": "Blunt output for Qwen Code: action first, numbered steps, no AI tells, plain Simplified Technical English, and visible progress.", "skills": "skills" } \ No newline at end of file diff --git a/skills/blunt/SKILL.md b/skills/blunt/SKILL.md index 77d6560..541bf57 100644 --- a/skills/blunt/SKILL.md +++ b/skills/blunt/SKILL.md @@ -12,7 +12,7 @@ description: >- disable-model-invocation: true license: MIT metadata: - version: "1.0.0" + version: "1.1.0" category: "productivity" tags: "ADHD, output style, plain English, Simplified Technical English, AI tells" --- From 9498644ef6914860aa7558f90d383b76ee6da6f0 Mon Sep 17 00:00:00 2001 From: Hayden Luckenbach Date: Tue, 15 Sep 2026 22:07:49 -0500 Subject: [PATCH 2/2] test: smoke-check the persisted blunt preference --- scripts/check_extension.py | 47 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/scripts/check_extension.py b/scripts/check_extension.py index e8b824b..ef1138a 100755 --- a/scripts/check_extension.py +++ b/scripts/check_extension.py @@ -396,6 +396,32 @@ def main() -> None: finally: client.close() + # 1.1.0: the stop phrase above persisted the OFF choice. Verify the + # preference file and that a fresh session starts OFF without flags. + persisted_config = json.loads( + Path(agent_dir, "blunt.json").read_text(encoding="utf8") + ) + assert persisted_config.get("enabled") is False + + persisted_off = RpcClient(executable, env, "--no-session", *extension_args) + try: + _, persisted_off_events = persisted_off.request( + "persisted-off", {"type": "get_state"}, + ) + assert not any( + "BLUNT ON" in (text or "") + for text in status_texts(persisted_off_events) + ) + entries, _ = persisted_off.request( + "entries-persisted-off", {"type": "get_entries"}, + ) + assert message_count(entries, "blunt-rules") == 0 + finally: + persisted_off.close() + + # The legacy always-on flag still forces ON once the persisted + # preference is cleared. + Path(agent_dir, "blunt.json").unlink(missing_ok=True) Path(agent_dir, ".blunt-always").touch() always_on = RpcClient( executable, @@ -415,6 +441,27 @@ def main() -> None: finally: always_on.close() + # The persisted ON choice restores in a fresh session with no flags. + Path(agent_dir, ".blunt-always").unlink(missing_ok=True) + Path(agent_dir, "blunt.json").write_text( + json.dumps({"enabled": True}), encoding="utf8", + ) + persisted_on = RpcClient(executable, env, "--no-session", *extension_args) + try: + _, persisted_on_events = persisted_on.request( + "persisted-on", {"type": "get_state"}, + ) + assert any( + "BLUNT ON" in (text or "") + for text in status_texts(persisted_on_events) + ) + entries, _ = persisted_on.request( + "entries-persisted-on", {"type": "get_entries"}, + ) + assert message_count(entries, "blunt-rules") == 1 + finally: + persisted_on.close() + print(f"{args.runtime} extension smoke test passed") if __name__ == "__main__":