Skip to content

Commit 2afe11f

Browse files
feat: add personality and insights commands
- Introduced `/personality` command to switch agent tone with built-in and custom personalities. - Added `/insights` command for summarizing agent activity over a configurable time window. - Implemented six built-in personalities: concise, verbose, security, senior-reviewer, junior-mentor, and ship-it. - Custom personalities can be defined in Markdown files within project or global directories. - Insights command provides metrics on runs, actions, active time, and project breakdowns. - Updated documentation to reflect new features and usage examples. - Added tests for personality management and insights functionality.
1 parent 60b30f7 commit 2afe11f

13 files changed

Lines changed: 813 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,53 @@ For releases before v1.3.35, see [GitHub Releases](https://github.com/VladoIvank
1111
> as the social-share summary (IFTTT → X/Bluesky), capped at 220 chars.
1212
> If omitted, the feed falls back to the first paragraph.
1313
14+
## [2.0.3] — 2026-05-19
15+
16+
> Two Hermes-inspired additions: `/personality <name>` switches agent tone mid-conversation (concise, security-paranoid, senior-reviewer, junior-mentor, ship-it, verbose, or your own from `.codeep/personalities/*.md`), and `/insights [--days N]` summarises what you've been working on — runs, files, tools, projects.
17+
18+
### Added — `/personality` slash command
19+
20+
- **Six built-in personalities** that swap the agent's tone and
21+
priorities by appending a system-prompt addendum:
22+
- `concise` — no preamble, no filler, bullet-heavy
23+
- `verbose` — explains rationale + alternatives + caveats
24+
- `security` — treats every input as hostile, enumerates attack surface
25+
- `senior-reviewer` — pushes back on shortcuts, names things well
26+
- `junior-mentor` — explains as it goes, links to canonical docs
27+
- `ship-it` — picks first reasonable approach, defers cleanup
28+
- **Custom personalities** via `.codeep/personalities/<name>.md`
29+
(project) or `~/.codeep/personalities/<name>.md` (global). First
30+
`# Personality: Name` line becomes the display name; rest of the
31+
Markdown body is the prompt addendum. Capped at 64 KB per file.
32+
- **Persistence**: active personality lives in `config.activePersonality`
33+
so it survives session restarts. Clear with `/personality off`.
34+
- Usable from CLI TUI, Zed, and the VS Code extension via ACP.
35+
36+
### Added — `/insights [--days N]`
37+
38+
- **Activity summary** over a configurable window (default 7 days,
39+
capped at 365). Reads `~/.codeep/history/<id>.json` files written by
40+
every agent run, so output reflects actual tool actions rather than
41+
chat-message proxies.
42+
- Headline metrics: total runs, total tool actions, total active time,
43+
active-days density, average actions per run.
44+
- **By-project breakdown** sorted by active time — see which repo soaked
45+
up your week.
46+
- **Top tools** (read_file × 340, write_file × 80, …) and
47+
**most-touched files** (with `~` prefix for readability).
48+
- **Recent runs** list — 10 most recent with project, duration, and the
49+
user prompt that started them.
50+
- Per-session cost still lives in `/cost`; `/insights` is a deliberately
51+
history-only view (the in-memory token tracker doesn't survive a
52+
restart, so historical cost would be misleading).
53+
54+
### Surfaced
55+
56+
- Both commands appear in `/help`, `/` autocomplete, `Codeep-web`
57+
`/docs/commands`, VS Code Settings → Commands chips, and ACP
58+
`availableCommands`. Spot-check parity: typing `/per` or `/insi` in
59+
any client autocompletes to the right command.
60+
1461
## [2.0.2] — 2026-05-19
1562

1663
> Two big quality-of-life additions: Anthropic prompt caching is on by default (60–90% cheaper on cache-eligible input), and `/plan` lets you preview an agent's full plan before any file gets touched. Run `/go` to execute, or `/plan <revised task>` to refine.

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,64 @@ Then call it as `/sec-review src/api/login.ts` (or `/sec` via the alias).
526526
**Discovery:** `/commands` lists all available templates. Project files shadow
527527
global files with the same name. Aliases also work for autocomplete.
528528

529+
### Personalities (`/personality`, new in 2.0.3)
530+
531+
Swap how the agent talks and what it prioritises mid-conversation:
532+
533+
```
534+
/personality # list available
535+
/personality concise # short answers, no preamble
536+
/personality security # treat every input as hostile
537+
/personality senior-reviewer # push back on shortcuts, name things well
538+
/personality ship-it # pick first reasonable approach
539+
/personality off # back to default tone
540+
```
541+
542+
Six built-in presets: `concise`, `verbose`, `security`, `senior-reviewer`,
543+
`junior-mentor`, `ship-it`. The active one persists across sessions
544+
(stored in `~/.codeep/config.json` as `activePersonality`).
545+
546+
**Custom personalities** — drop a Markdown file in
547+
`.codeep/personalities/<name>.md` (project) or
548+
`~/.codeep/personalities/<name>.md` (global):
549+
550+
```markdown
551+
# Personality: PR Reviewer
552+
553+
You are reviewing a PR from a junior engineer:
554+
- Cite line numbers for every concern.
555+
- Suggest an alternative, don't just flag the problem.
556+
- Keep tone collaborative, not pedantic.
557+
- End with one thing the author did well.
558+
```
559+
560+
First `# Personality:` line is the display name; the rest is appended
561+
to the agent's system prompt verbatim when active. Project shadows
562+
global shadows built-in (by name).
563+
564+
### Activity Insights (`/insights`, new in 2.0.3)
565+
566+
Summarise what the agent has actually done for you over a window — runs,
567+
tool actions, projects touched, most-edited files — sourced from
568+
`~/.codeep/history/<id>.json` (one file per agent run, automatic).
569+
570+
```
571+
/insights # last 7 days (default)
572+
/insights --days 30 # last month
573+
/insights --days 1 # today only
574+
```
575+
576+
Surfaces (markdown rendered in chat):
577+
578+
- Headline tally: runs · actions · active time · active-days density · avg actions/run
579+
- **By project** sorted by active time
580+
- **Top tools** (read_file × 340, write_file × 80, …)
581+
- **Most-touched files** (with `~` prefix for readability)
582+
- **Recent runs** — 10 most recent with project, duration, and the user prompt that started them
583+
584+
Cost / token usage isn't in `/insights` (it lives in `/cost` per-session
585+
since the token tracker is in-memory). Insights is history-only.
586+
529587
### Project Intelligence (`/init`, `/scan`)
530588

531589
Initialize a project and scan it once to cache deep analysis for faster AI responses:

src/acp/commands.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,45 @@ Anything else the agent should know — edge cases, gotchas, things to double-ch
680680

681681
// ─── Export ────────────────────────────────────────────────────────────────
682682

683+
// ─── Personalities + insights (2.0.3) ─────────────────────────────────────
684+
685+
case 'personality': {
686+
const { formatPersonalityList, findPersonality } = await import('../utils/personalities.js');
687+
const sub = args[0]?.toLowerCase();
688+
if (!sub) {
689+
return { handled: true, response: formatPersonalityList(session.workspaceRoot) };
690+
}
691+
if (sub === 'off' || sub === 'none' || sub === 'clear') {
692+
config.set('activePersonality', null);
693+
return { handled: true, response: 'Personality cleared — agent uses default tone.' };
694+
}
695+
const p = findPersonality(sub, session.workspaceRoot);
696+
if (!p) {
697+
return { handled: true, response: `No personality named \`${sub}\`. Run \`/personality\` to see available.` };
698+
}
699+
config.set('activePersonality', p.name);
700+
return {
701+
handled: true,
702+
response: `Active personality: **${p.displayName}** (\`${p.name}\`, ${p.scope})\n\n_${p.description}_\n\nClear with \`/personality off\`.`,
703+
};
704+
}
705+
706+
case 'insights': {
707+
const { formatInsights } = await import('../utils/insights.js');
708+
let days = 7;
709+
for (let i = 0; i < args.length; i++) {
710+
const a = args[i];
711+
if (a === '--days' && args[i + 1]) {
712+
const n = parseInt(args[i + 1], 10);
713+
if (Number.isFinite(n)) days = n;
714+
} else if (a.startsWith('--days=')) {
715+
const n = parseInt(a.slice('--days='.length), 10);
716+
if (Number.isFinite(n)) days = n;
717+
}
718+
}
719+
return { handled: true, response: formatInsights({ days }) };
720+
}
721+
683722
// ─── Plan mode (2.0.2) ────────────────────────────────────────────────────
684723

685724
case 'plan': {

src/acp/server.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ const AVAILABLE_COMMANDS = [
7777
// Plan mode (2.0.2)
7878
{ name: 'plan', description: 'Generate a numbered plan for a task — review before /go executes', input: { hint: '<task>' } },
7979
{ name: 'go', description: 'Execute the pending plan from /plan' },
80+
// Personalities + insights (2.0.3)
81+
{ name: 'personality', description: 'List or switch agent tone preset', input: { hint: '[name | off]' } },
82+
{ name: 'insights', description: 'Activity summary over the last N days (default 7)', input: { hint: '[--days N]' } },
8083
// Project intelligence
8184
{ name: 'scan', description: 'Scan project structure and generate summary' },
8285
{ name: 'review', description: 'Run code review on project or specific files', input: { hint: '[file…]' } },

src/config/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ interface ConfigSchema {
8383
data_collection?: 'allow' | 'deny';
8484
require_parameters?: boolean;
8585
};
86+
/**
87+
* Active personality preset (`concise`, `senior-reviewer`, custom user
88+
* personalities from .codeep/personalities/*.md, …). When set, the
89+
* loader text is appended to every agent system prompt. See
90+
* utils/personalities.ts.
91+
*/
92+
activePersonality?: string | null;
8693
}
8794

8895
export type { AgentMode };

src/renderer/App.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
107107
'openrouter': 'Tune OpenRouter routing (preferred / ignore providers, fallbacks, privacy)',
108108
'plan': 'Generate a numbered plan for a task — review before /go executes it',
109109
'go': 'Execute the pending plan from /plan',
110+
'personality': 'Switch agent tone: concise / verbose / security / senior-reviewer / etc',
111+
'insights': 'Activity summary over the last N days (default 7): runs, files, tools, projects',
110112
};
111113

112114
import { helpCategories, keyboardShortcuts } from './components/Help';
@@ -301,6 +303,8 @@ export class App {
301303
'hooks', 'mcp', 'openrouter',
302304
// 2.0.2 — plan mode.
303305
'plan', 'go',
306+
// 2.0.3 — personalities + insights.
307+
'personality', 'insights',
304308
'c', 't', 'd', 'r', 'f', 'e', 'o', 'b', 'p',
305309
];
306310

src/renderer/commands.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,50 @@ export async function handleCommand(
248248
break;
249249
}
250250

251+
case 'insights': {
252+
const { formatInsights } = await import('../utils/insights');
253+
// Parse `--days N` (default 7). Accept both `--days 30` and `--days=30`.
254+
let days = 7;
255+
for (let i = 0; i < args.length; i++) {
256+
const a = args[i];
257+
if (a === '--days' && args[i + 1]) {
258+
const n = parseInt(args[i + 1], 10);
259+
if (Number.isFinite(n)) days = n;
260+
} else if (a.startsWith('--days=')) {
261+
const n = parseInt(a.slice('--days='.length), 10);
262+
if (Number.isFinite(n)) days = n;
263+
}
264+
}
265+
ctx.app.addMessage({ role: 'system', content: formatInsights({ days }) });
266+
break;
267+
}
268+
269+
case 'personality': {
270+
const { formatPersonalityList, findPersonality } = await import('../utils/personalities');
271+
const sub = args[0]?.toLowerCase();
272+
273+
if (!sub) {
274+
ctx.app.addMessage({ role: 'system', content: formatPersonalityList(ctx.projectPath) });
275+
break;
276+
}
277+
if (sub === 'off' || sub === 'none' || sub === 'clear') {
278+
config.set('activePersonality', null);
279+
ctx.app.notify('Personality cleared — agent uses default tone.');
280+
break;
281+
}
282+
const personality = findPersonality(sub, ctx.projectPath);
283+
if (!personality) {
284+
ctx.app.notify(`No personality named "${sub}". Run /personality to see available.`);
285+
break;
286+
}
287+
config.set('activePersonality', personality.name);
288+
ctx.app.addMessage({
289+
role: 'system',
290+
content: `Active personality: **${personality.displayName}** (\`${personality.name}\`, ${personality.scope})\n\n_${personality.description}_\n\nClear with \`/personality off\`.`,
291+
});
292+
break;
293+
}
294+
251295
case 'plan': {
252296
// Plan mode: ask the model for a plan, surface it, hold as pending.
253297
// The user runs /go to execute or /plan <revised> to revise. See

src/renderer/components/Help.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ export const helpCategories: HelpCategory[] = [
133133
{ key: '/profile save <name>', description: 'Save current provider+model as profile' },
134134
{ key: '/profile list', description: 'List saved profiles' },
135135
{ key: '/openrouter', description: 'OpenRouter routing prefs (prefer/ignore providers, fallbacks, privacy)' },
136+
{ key: '/personality', description: 'List or switch agent tone (concise / verbose / security / senior-reviewer / …)' },
137+
{ key: '/personality <name>', description: 'Activate a personality. /personality off to clear.' },
138+
{ key: '/insights [--days N]', description: 'Activity summary — runs, files, tools, projects over the last N days (default 7)' },
136139
],
137140
},
138141
{

src/utils/agent.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,20 @@ export async function runAgent(
358358
if (skillCatalogBlock) {
359359
systemPrompt += '\n\n' + skillCatalogBlock;
360360
}
361-
361+
362+
// Active personality goes LAST — appended after skills / project rules /
363+
// smart context so its tone overrides earlier conventions. Set via
364+
// `/personality <name>`; empty when no personality is active.
365+
try {
366+
const { getActivePersonalityPrompt } = await import('./personalities.js');
367+
const personalityPrompt = getActivePersonalityPrompt(projectContext.root);
368+
if (personalityPrompt) {
369+
systemPrompt += personalityPrompt;
370+
}
371+
} catch {
372+
// Personality loading must never block an agent run.
373+
}
374+
362375
// Initial user message with optional task plan
363376
let initialPrompt = prompt;
364377
if (taskPlan) {

src/utils/insights.test.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, readdirSync, unlinkSync } from 'fs';
3+
import { tmpdir, homedir } from 'os';
4+
import { join } from 'path';
5+
6+
// We can't easily redirect homedir() at runtime, so insights.test seeds
7+
// a unique-named history file in the REAL ~/.codeep/history/ and cleans
8+
// it up afterwards. This keeps the test honest (exercises real disk +
9+
// real paths) without colliding with the user's actual history.
10+
11+
const HISTORY_DIR = join(homedir(), '.codeep', 'history');
12+
const TEST_ID_PREFIX = '_insights_test_';
13+
14+
import { formatInsights } from './insights';
15+
16+
interface TestRun {
17+
id: string;
18+
startTime: number;
19+
endTime: number;
20+
prompt: string;
21+
projectRoot: string;
22+
actions: { id: string; timestamp: number; type: string; path?: string }[];
23+
}
24+
25+
function writeTestRun(run: TestRun): string {
26+
if (!existsSync(HISTORY_DIR)) mkdirSync(HISTORY_DIR, { recursive: true });
27+
const filename = `${TEST_ID_PREFIX}${run.id}.json`;
28+
writeFileSync(join(HISTORY_DIR, filename), JSON.stringify(run));
29+
return filename;
30+
}
31+
32+
function cleanupTestRuns(): void {
33+
if (!existsSync(HISTORY_DIR)) return;
34+
for (const f of readdirSync(HISTORY_DIR)) {
35+
if (f.startsWith(TEST_ID_PREFIX)) {
36+
try { unlinkSync(join(HISTORY_DIR, f)); } catch { /* best-effort */ }
37+
}
38+
}
39+
}
40+
41+
describe('insights', () => {
42+
beforeEach(() => cleanupTestRuns());
43+
afterEach(() => cleanupTestRuns());
44+
45+
it('renders empty state with hint when no runs in window', () => {
46+
const out = formatInsights({ days: 7 });
47+
// Real history dir may have other runs from the user; only assert
48+
// shape (headline + "_No agent runs_" OR aggregate metrics line).
49+
expect(out).toMatch(/^## Activity last 7 days/);
50+
});
51+
52+
it('aggregates runs across project, tools, and files', () => {
53+
const now = Date.now();
54+
const oneHourAgo = now - 3_600_000;
55+
writeTestRun({
56+
id: `${TEST_ID_PREFIX}a`,
57+
startTime: oneHourAgo,
58+
endTime: oneHourAgo + 120_000,
59+
prompt: 'add a new endpoint',
60+
projectRoot: '/Users/test/proj-a',
61+
actions: [
62+
{ id: 'x1', timestamp: oneHourAgo + 10_000, type: 'read', path: '/Users/test/proj-a/api.ts' },
63+
{ id: 'x2', timestamp: oneHourAgo + 20_000, type: 'write', path: '/Users/test/proj-a/api.ts' },
64+
{ id: 'x3', timestamp: oneHourAgo + 30_000, type: 'write', path: '/Users/test/proj-a/api.test.ts' },
65+
],
66+
});
67+
writeTestRun({
68+
id: `${TEST_ID_PREFIX}b`,
69+
startTime: oneHourAgo + 1_000,
70+
endTime: oneHourAgo + 60_000,
71+
prompt: 'fix the failing test',
72+
projectRoot: '/Users/test/proj-b',
73+
actions: [
74+
{ id: 'y1', timestamp: oneHourAgo + 5_000, type: 'execute', path: undefined },
75+
],
76+
});
77+
78+
const out = formatInsights({ days: 1 });
79+
// Headline tally includes our 2 runs (real user runs may also be
80+
// present, so assert ≥ not exact).
81+
expect(out).toMatch(/\*\*\d+\*\* runs?/);
82+
// Per-project section names our buckets (basename of projectRoot).
83+
expect(out).toContain('proj-a');
84+
expect(out).toContain('proj-b');
85+
// Top tools section surfaces the action types we wrote.
86+
expect(out).toContain('`write`');
87+
expect(out).toContain('`read`');
88+
expect(out).toContain('`execute`');
89+
// Most-touched files section abbreviates HOME prefix.
90+
expect(out).toContain('api.ts');
91+
// Recent runs section quotes the user prompt.
92+
expect(out).toContain('add a new endpoint');
93+
expect(out).toContain('fix the failing test');
94+
});
95+
96+
it('clamps --days to [1, 365]', () => {
97+
expect(formatInsights({ days: 0 })).toMatch(/last 1 day\b/);
98+
expect(formatInsights({ days: -10 })).toMatch(/last 1 day\b/);
99+
expect(formatInsights({ days: 9999 })).toMatch(/last 365 days/);
100+
});
101+
});

0 commit comments

Comments
 (0)