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-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion .cursor/skills/blunt/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
---
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
65 changes: 52 additions & 13 deletions extensions/blunt.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -29,7 +29,7 @@ const STOP_PHRASES: Record<string, true> = {
"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.";

Expand All @@ -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(
Expand Down Expand Up @@ -163,22 +185,39 @@ 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);
};

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", {
Expand All @@ -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();

Expand Down
2 changes: 1 addition & 1 deletion gemini-extension.json
Original file line number Diff line number Diff line change
@@ -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"
}
2 changes: 1 addition & 1 deletion kimi.plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion qwen-extension.json
Original file line number Diff line number Diff line change
@@ -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"
}
47 changes: 47 additions & 0 deletions scripts/check_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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__":
Expand Down
2 changes: 1 addition & 1 deletion skills/blunt/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
---
Expand Down
Loading