claude-code-session-manager 0.73.2 → 0.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/AgentLibrary-CSnWV6uv.js +3 -0
- package/dist/assets/{History-Dnx86P4n.js → History-ShwkUTny.js} +1 -1
- package/dist/assets/{Hooks-CAYlpqBp.js → Hooks-Dc5y1rZH.js} +1 -1
- package/dist/assets/{HostBilko-CXykS2ap.js → HostBilko-evd1hD8q.js} +1 -1
- package/dist/assets/{Library-DVbXlboP.js → Library-BOL6GqFX.js} +1 -1
- package/dist/assets/{ListDetail-DXd5rB2F.js → ListDetail-BPJb2et3.js} +1 -1
- package/dist/assets/{MarkdownEditor-D0mO1TUZ.js → MarkdownEditor-CACsHVuf.js} +1 -1
- package/dist/assets/{McpServers-BxUk1aR3.js → McpServers-DuOTxmc4.js} +1 -1
- package/dist/assets/{Memory-DVbPQFZw.js → Memory-BYJHDz7W.js} +1 -1
- package/dist/assets/{Panel-DigZB9Yd.js → Panel-n-p8OQLc.js} +1 -1
- package/dist/assets/{Permissions-CSLxAoTL.js → Permissions-DhYERDVR.js} +1 -1
- package/dist/assets/{Plugins-DOTiP9sQ.js → Plugins-D1ckVgo3.js} +2 -2
- package/dist/assets/{ProvenanceBadge-PsNd-CBq.js → ProvenanceBadge-ByTjDwAX.js} +1 -1
- package/dist/assets/{SaveBar-C4bgDklX.js → SaveBar-_ORUCmZw.js} +1 -1
- package/dist/assets/{Scheduler-CrBVVXN1.js → Scheduler-Bu6kSkl3.js} +7 -7
- package/dist/assets/{ScopeSwitcher-RMAwgmj4.js → ScopeSwitcher-Css6Sg7Y.js} +1 -1
- package/dist/assets/{Settings-CM4lZxnn.js → Settings-DlyJjexf.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-B1DaeT2R.js → SkillReferenceGraph-rKaTHmOg.js} +1 -1
- package/dist/assets/{Skills-CSdxEC-f.js → Skills-BHlH2uG5.js} +1 -1
- package/dist/assets/{SystemPrompt-j-WYOC5f.js → SystemPrompt-DsgF8xsl.js} +1 -1
- package/dist/assets/TagLibrary-Q5QsKMes.js +1 -0
- package/dist/assets/{TiptapBody-4hzD0jjv.js → TiptapBody-XoQHiqfF.js} +1 -1
- package/dist/assets/{Toggle-H6pNcv3l.js → Toggle-BJHhHQOq.js} +1 -1
- package/dist/assets/{index-DJ6xdB0r.js → index-Cs_Lq9-w.js} +3 -3
- package/dist/assets/{settingsSchema-DVblsXFp.js → settingsSchema-BTDJcu2x.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/plugins/session-manager-dev/skills/memory-sanitation/SKILL.md +21 -1
- package/src/main/__tests__/agentModelResolve.test.cjs +19 -0
- package/src/main/__tests__/crossProjectFeedback.test.cjs +7 -0
- package/src/main/__tests__/crossProjectFeedbackRoutes.test.cjs +7 -0
- package/src/main/__tests__/epicMint.test.cjs +50 -2
- package/src/main/__tests__/health-claude-md-budget.test.cjs +57 -0
- package/src/main/__tests__/prdCreate.test.cjs +17 -0
- package/src/main/__tests__/promptSessionsCreateEpicHandler.test.cjs +7 -0
- package/src/main/__tests__/workTypeLibrary.test.cjs +89 -0
- package/src/main/health.cjs +51 -1
- package/src/main/index.cjs +2 -2
- package/src/main/ipcSchemas.cjs +21 -9
- package/src/main/lib/__tests__/agentPersonaSchema.test.cjs +67 -0
- package/src/main/lib/agentModelResolve.cjs +32 -1
- package/src/main/lib/agentPersonaSchema.cjs +82 -0
- package/src/main/lib/epicMint.cjs +74 -1
- package/src/main/lib/prdFrontmatter.cjs +6 -5
- package/src/main/lib/promptSessionSchema.cjs +6 -14
- package/src/main/lib/workTypeLibrary.cjs +48 -0
- package/src/preload/api.d.ts +7 -2
- package/dist/assets/AgentLibrary-D0frFrHG.js +0 -3
- package/dist/assets/TagLibrary-Fj0zrPru.js +0 -1
package/src/main/health.cjs
CHANGED
|
@@ -181,6 +181,37 @@ function evaluatePerProjectStall(stallSummary, lastRunAtIso, now, thresholdMs) {
|
|
|
181
181
|
return results;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
// Parses the "SIZE BUDGET — 12,000 chars" header line out of a project's
|
|
185
|
+
// CLAUDE.md text. Tolerant of the em-dash, bold markers, the leading
|
|
186
|
+
// blockquote '>', and the thousands comma. Returns null (not a throw, not a
|
|
187
|
+
// default) when the line is absent or unparseable — a CLAUDE.md that
|
|
188
|
+
// predates this convention has nothing to enforce, and that must stay a
|
|
189
|
+
// silent skip rather than a manufactured RED.
|
|
190
|
+
function parseClaudeMdBudget(text) {
|
|
191
|
+
if (typeof text !== 'string') return null;
|
|
192
|
+
const match = text.match(/SIZE BUDGET\s*[—-]+\s*([\d,]+)\s*chars/i);
|
|
193
|
+
if (!match) return null;
|
|
194
|
+
const n = Number(match[1].replace(/,/g, ''));
|
|
195
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Pure comparison of CLAUDE.md's actual byte size against its self-declared
|
|
199
|
+
// budget (see parseClaudeMdBudget). budget === null means "no budget
|
|
200
|
+
// declared" — skip silently rather than treating it as 0 or infinite.
|
|
201
|
+
function evaluateClaudeMdBudget(chars, budget) {
|
|
202
|
+
if (budget == null) return { ok: true, applicable: false };
|
|
203
|
+
const overage = chars - budget;
|
|
204
|
+
if (overage <= 0) return { ok: true, applicable: true, chars, budget };
|
|
205
|
+
return {
|
|
206
|
+
ok: false,
|
|
207
|
+
applicable: true,
|
|
208
|
+
chars,
|
|
209
|
+
budget,
|
|
210
|
+
overage,
|
|
211
|
+
message: `CLAUDE.md is ${chars} chars, over its ${budget}-char budget by ${overage} (see the SIZE BUDGET note in CLAUDE.md)`,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
184
215
|
// Pure evaluator over migratePrds()'s { moved, skipped, unresolved } result —
|
|
185
216
|
// kept separate from the fs-touching check() call site so it's directly
|
|
186
217
|
// unit-testable, matching evaluateTickLiveness's pattern.
|
|
@@ -443,6 +474,23 @@ async function check() {
|
|
|
443
474
|
};
|
|
444
475
|
}
|
|
445
476
|
|
|
477
|
+
// 6.4. Check project CLAUDE.md against its own self-declared SIZE BUDGET
|
|
478
|
+
// header (see the CLAUDE.md size-budget note). Skips silently when no
|
|
479
|
+
// budget line is present — this is not required for every project.
|
|
480
|
+
try {
|
|
481
|
+
const claudeMdPath = path.join(PROJECT_ROOT, 'CLAUDE.md');
|
|
482
|
+
const claudeMdText = fs.readFileSync(claudeMdPath, 'utf8');
|
|
483
|
+
const chars = Buffer.byteLength(claudeMdText, 'utf8');
|
|
484
|
+
const budget = parseClaudeMdBudget(claudeMdText);
|
|
485
|
+
const budgetResult = evaluateClaudeMdBudget(chars, budget);
|
|
486
|
+
status.components.claude_md_budget = budgetResult;
|
|
487
|
+
if (!budgetResult.ok) {
|
|
488
|
+
status.issues.push(budgetResult.message);
|
|
489
|
+
}
|
|
490
|
+
} catch (e) {
|
|
491
|
+
status.components.claude_md_budget = { ok: true, applicable: false };
|
|
492
|
+
}
|
|
493
|
+
|
|
446
494
|
// 6.5. Check ~/.claude/CLAUDE.md's @import chain resolves cleanly.
|
|
447
495
|
// Informational only — a broken/stale persona import degrades instruction
|
|
448
496
|
// fidelity, not app health, so it never flips status.ok to false. See
|
|
@@ -492,7 +540,7 @@ async function check() {
|
|
|
492
540
|
// Critical: nodejs, config dir, typescript, build artifact, test infrastructure.
|
|
493
541
|
// Non-fatal: scheduler/transcripts dirs may not exist on fresh install.
|
|
494
542
|
// Informational: app log age (shows if app is running, but not blocking).
|
|
495
|
-
const criticalComponents = ['nodejs', 'config_dir', 'typescript', 'build_artifact', 'test_infrastructure', 'scheduler_queue', 'prd_migration'];
|
|
543
|
+
const criticalComponents = ['nodejs', 'config_dir', 'typescript', 'build_artifact', 'test_infrastructure', 'scheduler_queue', 'prd_migration', 'claude_md_budget'];
|
|
496
544
|
status.ok = criticalComponents.every((c) => status.components[c]?.ok !== false);
|
|
497
545
|
|
|
498
546
|
status.elapsedMs = Date.now() - start;
|
|
@@ -515,6 +563,8 @@ module.exports = {
|
|
|
515
563
|
evaluatePrdMigrationHealth,
|
|
516
564
|
computeProjectProblemCounts,
|
|
517
565
|
evaluatePerProjectStall,
|
|
566
|
+
parseClaudeMdBudget,
|
|
567
|
+
evaluateClaudeMdBudget,
|
|
518
568
|
TICK_STALL_THRESHOLD_MS,
|
|
519
569
|
HEARTBEAT_STALE_MS,
|
|
520
570
|
};
|
package/src/main/index.cjs
CHANGED
|
@@ -466,11 +466,11 @@ ipcMain.handle('agents:list-personas', () => agentLibrary.listPersonas());
|
|
|
466
466
|
function broadcastAgentsChanged() {
|
|
467
467
|
sendIfAlive(mainWindow, 'agents:changed', {});
|
|
468
468
|
}
|
|
469
|
-
ipcMain.handle('agents:save-persona', async (
|
|
469
|
+
ipcMain.handle('agents:save-persona', validated(schemas.agentsSavePersona, async (payload) => {
|
|
470
470
|
const result = await agentLibrary.savePersona(payload);
|
|
471
471
|
broadcastAgentsChanged();
|
|
472
472
|
return result;
|
|
473
|
-
});
|
|
473
|
+
}));
|
|
474
474
|
ipcMain.handle('agents:delete-persona', async (_e, payload) => {
|
|
475
475
|
const result = await agentLibrary.deletePersona(payload);
|
|
476
476
|
broadcastAgentsChanged();
|
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -9,6 +9,8 @@ const { z } = require('zod');
|
|
|
9
9
|
const os = require('node:os');
|
|
10
10
|
const path = require('node:path');
|
|
11
11
|
const { PromptSessionSchema, EpicTagSchema, EpicSourceSchema, EpicIntakeSectionSchema } = require('./lib/promptSessionSchema.cjs');
|
|
12
|
+
const { PrdWorkTypeSchema } = require('./lib/workTypeLibrary.cjs');
|
|
13
|
+
const { AgentPersonaSaveSchema } = require('./lib/agentPersonaSchema.cjs');
|
|
12
14
|
|
|
13
15
|
// ──────────────────────────────────────────── PTY
|
|
14
16
|
const ptySpawn = z.object({
|
|
@@ -331,13 +333,13 @@ const schedulerCreatePrd = z.object({
|
|
|
331
333
|
// within an Epic's own chat session joins that Epic even if the tool call
|
|
332
334
|
// forgot to pass sourcePromptId explicitly.
|
|
333
335
|
originClaudeSessionId: z.string().min(1).max(128).regex(NO_NEWLINE_RE, 'must not contain newlines').optional(),
|
|
334
|
-
// User-selected
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
tag:
|
|
336
|
+
// User-selected work-type tag (PRD 774; independence from the parent
|
|
337
|
+
// Epic's own tag confirmed PRD 1041) — deterministic, never LLM-classified.
|
|
338
|
+
// Epic tag and PRD tag draw on the same WorkType vocabulary
|
|
339
|
+
// (lib/workTypeLibrary.cjs) but are independent values; 'discussion' is
|
|
340
|
+
// excluded here for the same reason adminPrdFrontmatterPatch excludes it
|
|
341
|
+
// below — a discussion-tagged Epic never reaches PRD authoring.
|
|
342
|
+
tag: PrdWorkTypeSchema.optional(),
|
|
341
343
|
});
|
|
342
344
|
|
|
343
345
|
// Bulk archive: slug list, capped to limit unbounded retag/archive payloads.
|
|
@@ -387,8 +389,9 @@ const adminPrdFrontmatterPatch = z.object({
|
|
|
387
389
|
sourceTabId: z.string().min(1).max(128).regex(NO_NEWLINE_RE, 'must not contain newlines').optional(),
|
|
388
390
|
// 'discussion' deliberately excluded: matches prdFrontmatter.cjs's
|
|
389
391
|
// parsePrdFile, which never parses that value for a PRD's tag key (a
|
|
390
|
-
// discussion-tagged Epic never reaches PRD authoring)
|
|
391
|
-
|
|
392
|
+
// discussion-tagged Epic never reaches PRD authoring) — see
|
|
393
|
+
// workTypeLibrary.cjs's PrdWorkTypeSchema.
|
|
394
|
+
tag: PrdWorkTypeSchema.optional(),
|
|
392
395
|
// Provenance stamp (PRD-authoring lockdown). Free-text rather than an enum
|
|
393
396
|
// — 'scheduler-api' (create-time) and 'legacy-adopted' (migration/manual
|
|
394
397
|
// adopt) are the two values this codebase writes today, but the field
|
|
@@ -464,6 +467,14 @@ const setSessionSlotsSchema = z.object({
|
|
|
464
467
|
cap: z.number().int().min(0).max(10),
|
|
465
468
|
}).strict();
|
|
466
469
|
|
|
470
|
+
// ──────────────────────────────────────────── Agent Library
|
|
471
|
+
// agents:save-persona — the Agent entity (Agent/WorkType/Epic/PRD/Job ERD)
|
|
472
|
+
// had no boundary validation until this schema; Epic and Job each got theirs
|
|
473
|
+
// only after a production incident (see scheduleJobSchema.cjs's header).
|
|
474
|
+
// Full field-level rules live in lib/agentPersonaSchema.cjs, mirroring
|
|
475
|
+
// AgentPersonaSaveInput (src/preload/api.d.ts) field-for-field.
|
|
476
|
+
const agentsSavePersona = AgentPersonaSaveSchema;
|
|
477
|
+
|
|
467
478
|
// ──────────────────────────────────────────── Memory tool (Bundle C, cycle 3)
|
|
468
479
|
// Workspace-scoped markdown store at ~/.claude/projects/<ws>/memory/.
|
|
469
480
|
// Slug regex is shared with memoryTool.cjs/memoryAggregate.cjs via lib/memorySlug.cjs;
|
|
@@ -962,6 +973,7 @@ module.exports = {
|
|
|
962
973
|
chatClassifyTicket,
|
|
963
974
|
chatExternalSend,
|
|
964
975
|
exchangesList,
|
|
976
|
+
agentsSavePersona,
|
|
965
977
|
},
|
|
966
978
|
validated,
|
|
967
979
|
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agentPersonaSchema.test.cjs — unit tests for the canonical Agent persona
|
|
3
|
+
* WRITE-path zod schema and its assertValidAgentPersonaSave fail-closed
|
|
4
|
+
* helper.
|
|
5
|
+
*
|
|
6
|
+
* Agent is the third entity in the Agent/WorkType/Epic/PRD/Job ERD to get a
|
|
7
|
+
* boundary schema — Epic (promptSessionSchema.cjs) and Job
|
|
8
|
+
* (scheduleJobSchema.cjs) each got theirs only after a production incident;
|
|
9
|
+
* this one closes the gap proactively before Agent has had its own.
|
|
10
|
+
*
|
|
11
|
+
* Run: timeout 120 npx vitest run src/main/lib/__tests__/agentPersonaSchema.test.cjs
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
import { test, expect } from 'vitest';
|
|
17
|
+
const {
|
|
18
|
+
AgentPersonaSaveSchema,
|
|
19
|
+
assertValidAgentPersonaSave,
|
|
20
|
+
} = require('../agentPersonaSchema.cjs');
|
|
21
|
+
|
|
22
|
+
function validPersona(overrides = {}) {
|
|
23
|
+
return {
|
|
24
|
+
name: 'code-reviewer',
|
|
25
|
+
description: 'Reviews diffs for correctness and style.',
|
|
26
|
+
tools: ['Read', 'Grep', 'Glob'],
|
|
27
|
+
model: 'inherit',
|
|
28
|
+
color: 'blue',
|
|
29
|
+
tags: ['feature', 'bug'],
|
|
30
|
+
projects: ['*'],
|
|
31
|
+
action: '/develop\n\nReview the current diff.',
|
|
32
|
+
actionLabel: 'Review',
|
|
33
|
+
title: 'Engineering — Code Reviewer',
|
|
34
|
+
body: 'You are a meticulous code reviewer.',
|
|
35
|
+
...overrides,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
test('a valid full persona parses', () => {
|
|
40
|
+
const full = validPersona();
|
|
41
|
+
expect(() => assertValidAgentPersonaSave(full)).not.toThrow();
|
|
42
|
+
expect(AgentPersonaSaveSchema.safeParse(full).success).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('a persona with an unknown tag is rejected', () => {
|
|
46
|
+
const badTag = validPersona({ tags: ['feature', 'not-a-real-worktype'] });
|
|
47
|
+
expect(() => assertValidAgentPersonaSave(badTag)).toThrow(/tags/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('a persona with an uppercase/spaced name is rejected', () => {
|
|
51
|
+
const badName = validPersona({ name: 'Code Reviewer' });
|
|
52
|
+
expect(() => assertValidAgentPersonaSave(badName)).toThrow(/name/);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('a persona with only the required fields parses', () => {
|
|
56
|
+
const minimal = {
|
|
57
|
+
name: 'minimal-agent',
|
|
58
|
+
description: '',
|
|
59
|
+
tools: [],
|
|
60
|
+
model: 'inherit',
|
|
61
|
+
color: '',
|
|
62
|
+
tags: [],
|
|
63
|
+
body: '',
|
|
64
|
+
};
|
|
65
|
+
expect(() => assertValidAgentPersonaSave(minimal)).not.toThrow();
|
|
66
|
+
expect(AgentPersonaSaveSchema.safeParse(minimal).success).toBe(true);
|
|
67
|
+
});
|
|
@@ -27,6 +27,35 @@ const configMgr = require('../config.cjs');
|
|
|
27
27
|
/** The hardcoded floor every branch of resolveEpicModel falls back to — --model must never be omitted (CLAUDE.md "Automation model pinning"). */
|
|
28
28
|
const FALLBACK_MODEL = 'sonnet';
|
|
29
29
|
|
|
30
|
+
// READ side of the agentType FK (write side: epicMint.cjs's ensureEpic —
|
|
31
|
+
// "throw on write, report on read"). A persona can legitimately be renamed
|
|
32
|
+
// or deleted after an Epic was created against it; that Epic must keep
|
|
33
|
+
// loading (this module's whole contract is "never throws"), but the
|
|
34
|
+
// dangling reference should still be visible somewhere. Logged at most once
|
|
35
|
+
// per (cwd, agentType) so a headless launch that resolves the same Epic's
|
|
36
|
+
// model on every turn doesn't flood opsErrorLog's daily file.
|
|
37
|
+
const loggedDanglingPersonas = new Set();
|
|
38
|
+
|
|
39
|
+
function logDanglingPersonaOnce(cwd, agentType) {
|
|
40
|
+
if (!cwd || !agentType) return;
|
|
41
|
+
const key = `${cwd}::${agentType}`;
|
|
42
|
+
if (loggedDanglingPersonas.has(key)) return;
|
|
43
|
+
loggedDanglingPersonas.add(key);
|
|
44
|
+
try {
|
|
45
|
+
// Required lazily: opsErrorLog.cjs is a leaf module with no Electron
|
|
46
|
+
// dependency, but keeping this require inside the miss path avoids
|
|
47
|
+
// paying for it on the (overwhelmingly common) hit path.
|
|
48
|
+
const { appendError } = require('./opsErrorLog.cjs');
|
|
49
|
+
appendError({
|
|
50
|
+
cwd,
|
|
51
|
+
scope: 'agentModelResolve',
|
|
52
|
+
level: 'warn',
|
|
53
|
+
message: `agentType '${agentType}' has no resolvable persona file — model resolution fell back to '${FALLBACK_MODEL}'`,
|
|
54
|
+
meta: { agentType },
|
|
55
|
+
});
|
|
56
|
+
} catch { /* logging must never break model resolution */ }
|
|
57
|
+
}
|
|
58
|
+
|
|
30
59
|
/**
|
|
31
60
|
* Looks up the agentType an Epic (PromptSession) was created with, by the
|
|
32
61
|
* Epic's own claudeSessionId — the same domain-model join `prdCreate.cjs`'s
|
|
@@ -56,12 +85,14 @@ function readPersonaModel(agentType, deps = {}) {
|
|
|
56
85
|
try {
|
|
57
86
|
real = validatePath(path.join(globalDir, `${agentType}.md`));
|
|
58
87
|
} catch {
|
|
88
|
+
logDanglingPersonaOnce(deps.cwd, agentType);
|
|
59
89
|
return null;
|
|
60
90
|
}
|
|
61
91
|
let text;
|
|
62
92
|
try {
|
|
63
93
|
text = fs.readFileSync(real, 'utf8');
|
|
64
94
|
} catch {
|
|
95
|
+
logDanglingPersonaOnce(deps.cwd, agentType);
|
|
65
96
|
return null;
|
|
66
97
|
}
|
|
67
98
|
const { fm } = splitFrontmatter(text);
|
|
@@ -81,7 +112,7 @@ function resolveEpicModel({ cwd, claudeSessionId, fallbackModel = FALLBACK_MODEL
|
|
|
81
112
|
try {
|
|
82
113
|
const agentType = findAgentTypeByClaudeSessionId(cwd, claudeSessionId, deps);
|
|
83
114
|
if (!agentType) return fallbackModel;
|
|
84
|
-
const model = readPersonaModel(agentType, deps);
|
|
115
|
+
const model = readPersonaModel(agentType, { ...deps, cwd });
|
|
85
116
|
if (!model || model === 'inherit') return fallbackModel;
|
|
86
117
|
return model;
|
|
87
118
|
} catch {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical runtime schema for an Agent persona WRITE payload, mirroring the
|
|
3
|
+
* TS `AgentPersonaSaveInput` interface at src/preload/api.d.ts:229-246
|
|
4
|
+
* field-for-field.
|
|
5
|
+
*
|
|
6
|
+
* Modelled on promptSessionSchema.cjs / scheduleJobSchema.cjs's "zod schema
|
|
7
|
+
* asserted at the main-process boundary" pattern: Epic and Job each got this
|
|
8
|
+
* guarantee only after a production incident (see scheduleJobSchema.cjs's
|
|
9
|
+
* header for the Job one). Agent is the third ERD entity
|
|
10
|
+
* (Agent/WorkType/Epic/PRD/Job) and, until this schema, the only one with
|
|
11
|
+
* zero validation between the renderer and disk — `savePersona`
|
|
12
|
+
* (agentLibrary.cjs) checked only that `name` was kebab-case.
|
|
13
|
+
*
|
|
14
|
+
* This schema covers the WRITE path only (`agents.savePersona`). The READ
|
|
15
|
+
* path (`listPersonas`) stays permissive on purpose — personas on disk may be
|
|
16
|
+
* hand-written or authored by Claude Code itself and must keep loading even
|
|
17
|
+
* if they carry fields this app doesn't recognize.
|
|
18
|
+
*
|
|
19
|
+
* Plain CJS, no Electron dependency, so it's requirable from ipcSchemas.cjs
|
|
20
|
+
* the same way promptSessionSchema.cjs and scheduleJobSchema.cjs already are.
|
|
21
|
+
*/
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const { z } = require('zod');
|
|
25
|
+
const { WorkTypeSchema } = require('./workTypeLibrary.cjs');
|
|
26
|
+
const { PERSONA_NAME_RE, ALL_PROJECTS } = require('../agentLibrary.cjs');
|
|
27
|
+
|
|
28
|
+
// Bounded free-text fields — generous for legitimate persona authoring while
|
|
29
|
+
// still blocking a renderer from writing an effectively-unbounded frontmatter
|
|
30
|
+
// value (mirrors ipcSchemas.cjs's title/description caps elsewhere).
|
|
31
|
+
const PersonaNameSchema = z.string().regex(PERSONA_NAME_RE, 'agent name must be lowercase, hyphenated (e.g. "my-agent")');
|
|
32
|
+
const BoundedString = z.string().max(2000);
|
|
33
|
+
|
|
34
|
+
// `projects` entries are either the `'*'` sentinel (every project) or an
|
|
35
|
+
// absolute path — agentLibrary.cjs's openProjects always deals in absolute
|
|
36
|
+
// cwds, and a relative entry could never match one.
|
|
37
|
+
const ProjectEntrySchema = z.string().refine(
|
|
38
|
+
(v) => v === ALL_PROJECTS || v.startsWith('/'),
|
|
39
|
+
`must be "${ALL_PROJECTS}" or an absolute path`,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Mirrors AgentPersonaSaveInput (src/preload/api.d.ts:229-246). `tags` is the
|
|
43
|
+
// Epic-mission WorkType union, NOT a free-form string list — the same
|
|
44
|
+
// concept as PromptSessionSchema's `tag` / scheduleRetagPrd's `tag`, just
|
|
45
|
+
// plural here since a persona can be associated with more than one mission.
|
|
46
|
+
const AgentPersonaSaveSchema = z.object({
|
|
47
|
+
name: PersonaNameSchema,
|
|
48
|
+
// Previous filename, when renaming an existing persona. Omit when creating.
|
|
49
|
+
originalName: PersonaNameSchema.optional(),
|
|
50
|
+
description: BoundedString,
|
|
51
|
+
tools: z.array(BoundedString),
|
|
52
|
+
model: BoundedString,
|
|
53
|
+
color: BoundedString,
|
|
54
|
+
tags: z.array(WorkTypeSchema),
|
|
55
|
+
projects: z.array(ProjectEntrySchema).optional(),
|
|
56
|
+
action: z.string().max(20000).optional(),
|
|
57
|
+
actionLabel: BoundedString.optional(),
|
|
58
|
+
title: BoundedString.optional(),
|
|
59
|
+
// Unbounded — a persona's body is arbitrary system-prompt prose.
|
|
60
|
+
body: z.string(),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Throws a clear, descriptive error (not a raw ZodError dump) when `persona`
|
|
65
|
+
* doesn't match AgentPersonaSaveSchema. Mirrors assertValidPromptSession /
|
|
66
|
+
* assertValidScheduleJob's shape and message format exactly.
|
|
67
|
+
*/
|
|
68
|
+
function assertValidAgentPersonaSave(persona) {
|
|
69
|
+
const result = AgentPersonaSaveSchema.safeParse(persona);
|
|
70
|
+
if (!result.success) {
|
|
71
|
+
const issues = result.error.issues
|
|
72
|
+
.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
|
|
73
|
+
.join('; ');
|
|
74
|
+
throw new Error(`assertValidAgentPersonaSave: invalid AgentPersonaSave shape — ${issues}`);
|
|
75
|
+
}
|
|
76
|
+
return result.data;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
AgentPersonaSaveSchema,
|
|
81
|
+
assertValidAgentPersonaSave,
|
|
82
|
+
};
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
'use strict';
|
|
29
29
|
|
|
30
30
|
const fs = require('node:fs');
|
|
31
|
+
const os = require('node:os');
|
|
31
32
|
const path = require('node:path');
|
|
32
33
|
const crypto = require('node:crypto');
|
|
33
34
|
const { resolveEpicPrdWriteDir } = require('./prdLocations.cjs');
|
|
@@ -117,6 +118,59 @@ function hasOwn(obj, key) {
|
|
|
117
118
|
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
/**
|
|
122
|
+
* safeAgentMdPath(baseDir, agentType) — `<baseDir>/<agentType>.md`, refused
|
|
123
|
+
* (returns null) if resolving it would escape baseDir (a `..`-laced
|
|
124
|
+
* agentType). Deliberately NOT config.cjs's validatePath: that guards
|
|
125
|
+
* allowedRoots (home dir + every cwd a TAB has opened), a registry ensureEpic
|
|
126
|
+
* cannot assume the caller's cwd is already in — epicMint.cjs is a plain
|
|
127
|
+
* Node module callable from scheduler/watchdog contexts with a trusted cwd
|
|
128
|
+
* that was never opened as a TAB. Containment within baseDir is the only
|
|
129
|
+
* property that actually matters here.
|
|
130
|
+
*/
|
|
131
|
+
function safeAgentMdPath(baseDir, agentType) {
|
|
132
|
+
const resolvedBase = path.resolve(baseDir);
|
|
133
|
+
const withSep = resolvedBase.endsWith(path.sep) ? resolvedBase : resolvedBase + path.sep;
|
|
134
|
+
const resolved = path.resolve(resolvedBase, `${agentType}.md`);
|
|
135
|
+
return resolved.startsWith(withSep) ? resolved : null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* resolvePersonaPaths(cwd, agentType, deps) — the two on-disk locations an
|
|
140
|
+
* agentType name could resolve to, project overlay first: matches the
|
|
141
|
+
* precedence agentLibrary.cjs already documents (a project overlay at
|
|
142
|
+
* `<cwd>/.claude/agents/<name>.md` wins over the global
|
|
143
|
+
* `~/.claude/agents/<name>.md`).
|
|
144
|
+
*/
|
|
145
|
+
function resolvePersonaPaths(cwd, agentType, deps = {}) {
|
|
146
|
+
const projectDir = deps.projectDir || path.join(cwd, '.claude', 'agents');
|
|
147
|
+
const globalDir = deps.globalDir || path.join(os.homedir(), '.claude', 'agents');
|
|
148
|
+
return {
|
|
149
|
+
projectPath: path.join(projectDir, `${agentType}.md`),
|
|
150
|
+
globalPath: path.join(globalDir, `${agentType}.md`),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* personaFileExists(cwd, agentType, deps) — true iff `agentType` resolves to
|
|
156
|
+
* a readable `.md` persona file, checking the project overlay before the
|
|
157
|
+
* global agents dir. Never throws (a traversal attempt or missing file is
|
|
158
|
+
* treated as "doesn't exist", same fail-closed spirit as
|
|
159
|
+
* agentModelResolve.cjs's readPersonaModel).
|
|
160
|
+
*/
|
|
161
|
+
function personaFileExists(cwd, agentType, deps = {}) {
|
|
162
|
+
const projectDir = deps.projectDir || path.join(cwd, '.claude', 'agents');
|
|
163
|
+
const globalDir = deps.globalDir || path.join(os.homedir(), '.claude', 'agents');
|
|
164
|
+
for (const baseDir of [projectDir, globalDir]) {
|
|
165
|
+
const real = safeAgentMdPath(baseDir, agentType);
|
|
166
|
+
if (!real) continue;
|
|
167
|
+
try {
|
|
168
|
+
if (fs.statSync(real).isFile()) return true;
|
|
169
|
+
} catch { /* not found or unreadable — keep checking */ }
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
120
174
|
function slugify(text) {
|
|
121
175
|
return String(text || 'epic')
|
|
122
176
|
.toLowerCase()
|
|
@@ -168,7 +222,7 @@ function withPathLock(lockPath, task) {
|
|
|
168
222
|
* The Epic's id doubles as its directory name under scheduler/epics/, so the
|
|
169
223
|
* PromptSession ↔ on-disk Epic mapping is 1:1 with no lookup table.
|
|
170
224
|
*/
|
|
171
|
-
function ensureEpic(cwd, { goalText, tag, epicId: explicitEpicId, status = 'proposed', openingPrompt = null, sections = null, source = null, agentType = null, mintAuthority = null } = {}) {
|
|
225
|
+
function ensureEpic(cwd, { goalText, tag, epicId: explicitEpicId, status = 'proposed', openingPrompt = null, sections = null, source = null, agentType = null, mintAuthority = null } = {}, deps = {}) {
|
|
172
226
|
if (!cwd || typeof cwd !== 'string') throw new Error('ensureEpic: cwd is required');
|
|
173
227
|
// A relative cwd (e.g. a caller passing '.') would otherwise get stored
|
|
174
228
|
// verbatim on the minted Epic's `cwd` field — the renderer's EpicsWorkspace
|
|
@@ -229,6 +283,23 @@ function ensureEpic(cwd, { goalText, tag, epicId: explicitEpicId, status = 'prop
|
|
|
229
283
|
throw new Error(reason);
|
|
230
284
|
}
|
|
231
285
|
|
|
286
|
+
// WRITE-TIME FK CHECK (throw on write, report on read — see this file's
|
|
287
|
+
// header + agentModelResolve.cjs's readPersonaModel, which is the READ
|
|
288
|
+
// side of this same FK and stays permissive on purpose). A mint whose
|
|
289
|
+
// agentType names a persona that doesn't resolve is a bug in the caller
|
|
290
|
+
// (a typo, or a persona deleted between the picker loading and the mint
|
|
291
|
+
// landing) — refuse it now rather than silently orphaning the Epic.
|
|
292
|
+
if (agentType) {
|
|
293
|
+
const checkPersonaExists = deps.personaExists || personaFileExists;
|
|
294
|
+
if (!checkPersonaExists(cwd, agentType, deps)) {
|
|
295
|
+
const { projectPath, globalPath } = resolvePersonaPaths(cwd, agentType, deps);
|
|
296
|
+
const reason = `agentType '${agentType}' does not resolve to a readable persona file — checked `
|
|
297
|
+
+ `${projectPath} and ${globalPath}`;
|
|
298
|
+
appendAuditEvent('epic_mint_refused', { cwd, epicId: explicitEpicId ?? null, status, reason });
|
|
299
|
+
throw new Error(`ensureEpic: ${reason}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
232
303
|
const epicId = `${slugify(goalText)}-${crypto.randomUUID().slice(0, 8)}`;
|
|
233
304
|
const now = new Date().toISOString();
|
|
234
305
|
const session = {
|
|
@@ -358,6 +429,8 @@ module.exports = {
|
|
|
358
429
|
removeEpic,
|
|
359
430
|
activeIndexPath,
|
|
360
431
|
readActiveIndex,
|
|
432
|
+
personaFileExists,
|
|
433
|
+
resolvePersonaPaths,
|
|
361
434
|
// Exported so lib/activeIndexMerge.cjs's renderer-facing merge IPC handler
|
|
362
435
|
// serializes through the SAME lock instance as ensureEpic/
|
|
363
436
|
// appendPrdCreatedEvent (module-level Map, shared via Node's require
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
*/
|
|
24
24
|
'use strict';
|
|
25
25
|
|
|
26
|
+
const { PRD_WORK_TYPES } = require('./workTypeLibrary.cjs');
|
|
27
|
+
|
|
28
|
+
const PRD_TAG_VALUES = new Set(PRD_WORK_TYPES);
|
|
29
|
+
|
|
26
30
|
function splitFrontmatter(raw) {
|
|
27
31
|
if (typeof raw !== 'string' || !raw.startsWith('---\n')) {
|
|
28
32
|
return { fm: {}, body: raw, fmLineCount: 0 };
|
|
@@ -118,11 +122,8 @@ function applyKey(fm, key, after) {
|
|
|
118
122
|
fm.sourceTabId = String(v);
|
|
119
123
|
return;
|
|
120
124
|
case 'tag':
|
|
121
|
-
// Matches the TS source's applyKey exactly
|
|
122
|
-
|
|
123
|
-
// here even if present in a hand-edited file — see this file's header
|
|
124
|
-
// comment.
|
|
125
|
-
if (v === 'feature' || v === 'bug' || v === 'build') fm.tag = v;
|
|
125
|
+
// Matches the TS source's applyKey exactly — see PRD_TAG_VALUES above.
|
|
126
|
+
if (PRD_TAG_VALUES.has(v)) fm.tag = v;
|
|
126
127
|
return;
|
|
127
128
|
case 'createdVia':
|
|
128
129
|
fm.createdVia = String(v);
|
|
@@ -13,22 +13,14 @@
|
|
|
13
13
|
'use strict';
|
|
14
14
|
|
|
15
15
|
const { z } = require('zod');
|
|
16
|
+
const { WorkTypeSchema } = require('./workTypeLibrary.cjs');
|
|
16
17
|
|
|
17
18
|
// Mirrors EpicTag (src/renderer/lib/tagLibrary.ts:10) — the Epic-level
|
|
18
|
-
// mission tag
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
// definition rather than an import of that one.
|
|
24
|
-
const EpicTagSchema = z.enum([
|
|
25
|
-
'feature',
|
|
26
|
-
'bug',
|
|
27
|
-
'discussion',
|
|
28
|
-
'build',
|
|
29
|
-
'project-home-builder',
|
|
30
|
-
'bilko-host-publisher',
|
|
31
|
-
]);
|
|
19
|
+
// mission tag. Epic tag and PRD tag (ipcSchemas.cjs's scheduleRetagPrd `tag`
|
|
20
|
+
// field) are the same WorkType concept, so both validate against the shared
|
|
21
|
+
// WorkTypeSchema from workTypeLibrary.cjs rather than each declaring their
|
|
22
|
+
// own enum literal.
|
|
23
|
+
const EpicTagSchema = WorkTypeSchema;
|
|
32
24
|
|
|
33
25
|
// Mirrors EpicSource (src/renderer/state/promptSessions.ts:22-27).
|
|
34
26
|
const EpicSourceSchema = z.object({
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the WorkType id list — the Epic-mission tag
|
|
3
|
+
* (`PromptSession['tag']`, src/renderer/state/promptSessions.ts) and the PRD
|
|
4
|
+
* tag (`scheduleRetagPrd`'s `tag` field, ipcSchemas.cjs) are ONE concept, not
|
|
5
|
+
* two: a domain decision confirmed by the project owner. Both now validate
|
|
6
|
+
* against this same enum.
|
|
7
|
+
*
|
|
8
|
+
* `WORK_TYPES` mirrors the tag ids in src/renderer/lib/tagLibrary.ts's
|
|
9
|
+
* `TAG_LIBRARY`, in the same order — that file stays the human-facing SoT for
|
|
10
|
+
* labels/descriptions/developEagerness; this one is only the id list + zod
|
|
11
|
+
* enum, kept in sync with tagLibrary.ts by
|
|
12
|
+
* src/main/__tests__/workTypeLibrary.test.cjs (tagLibrary.ts is a renderer
|
|
13
|
+
* .ts module main-process .cjs can't import — see CLAUDE.md "no CommonJS in
|
|
14
|
+
* renderer, no ES modules in main").
|
|
15
|
+
*
|
|
16
|
+
* Plain CJS, no Electron dependency, so it's requirable from scheduler/CLI
|
|
17
|
+
* contexts — same constraint promptSessionSchema.cjs and scheduleJobSchema.cjs
|
|
18
|
+
* already satisfy.
|
|
19
|
+
*/
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const { z } = require('zod');
|
|
23
|
+
|
|
24
|
+
const WORK_TYPES = Object.freeze([
|
|
25
|
+
'feature',
|
|
26
|
+
'bug',
|
|
27
|
+
'discussion',
|
|
28
|
+
'build',
|
|
29
|
+
'project-home-builder',
|
|
30
|
+
'bilko-host-publisher',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const WorkTypeSchema = z.enum(WORK_TYPES);
|
|
34
|
+
|
|
35
|
+
// A 'discussion'-tagged ticket never reaches PRD authoring (a domain
|
|
36
|
+
// invariant unrelated to the Epic/PRD tag unification above), so the PRD
|
|
37
|
+
// frontmatter `tag` field — parsed in prdFrontmatter.cjs/.ts and patched via
|
|
38
|
+
// ipcSchemas.cjs's adminPrdFrontmatterPatch — excludes it even though
|
|
39
|
+
// WorkTypeSchema itself (used at PRD-create time) does not.
|
|
40
|
+
const PRD_WORK_TYPES = Object.freeze(WORK_TYPES.filter((t) => t !== 'discussion'));
|
|
41
|
+
const PrdWorkTypeSchema = z.enum(PRD_WORK_TYPES);
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
WORK_TYPES,
|
|
45
|
+
WorkTypeSchema,
|
|
46
|
+
PRD_WORK_TYPES,
|
|
47
|
+
PrdWorkTypeSchema,
|
|
48
|
+
};
|
package/src/preload/api.d.ts
CHANGED
|
@@ -199,6 +199,11 @@ export interface McpStatusResult {
|
|
|
199
199
|
checkedAt: number;
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
/** Mirrors WorkType (src/main/lib/workTypeLibrary.cjs) / EpicTag (tagLibrary.ts) — the
|
|
203
|
+
* same Epic-mission taxonomy, reused here since an Agent persona's `tags` field is that
|
|
204
|
+
* same concept, not a free-form string list. */
|
|
205
|
+
export type AgentPersonaTag = 'feature' | 'bug' | 'discussion' | 'build' | 'project-home-builder' | 'bilko-host-publisher';
|
|
206
|
+
|
|
202
207
|
/** One global `~/.claude/agents/<name>.md` persona, per the Agent Library nav page. */
|
|
203
208
|
export interface AgentPersona {
|
|
204
209
|
name: string;
|
|
@@ -207,7 +212,7 @@ export interface AgentPersona {
|
|
|
207
212
|
model: string | null;
|
|
208
213
|
color: string | null;
|
|
209
214
|
/** Epic intent tags (tagLibrary.ts's TAG_LIBRARY) this persona is associated with. */
|
|
210
|
-
tags:
|
|
215
|
+
tags: AgentPersonaTag[];
|
|
211
216
|
/** Project cwds whose Sessions toolbar shows a one-click Action button for this
|
|
212
217
|
* agent. The single entry `'*'` means every project. Empty = no Action button. */
|
|
213
218
|
projects: string[];
|
|
@@ -234,7 +239,7 @@ export interface AgentPersonaSaveInput {
|
|
|
234
239
|
tools: string[];
|
|
235
240
|
model: string;
|
|
236
241
|
color: string;
|
|
237
|
-
tags:
|
|
242
|
+
tags: AgentPersonaTag[];
|
|
238
243
|
/** Project cwds (or the `'*'` sentinel) this persona's Action button appears in. */
|
|
239
244
|
projects?: string[];
|
|
240
245
|
/** Opening instruction sent by this persona's Action button. */
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
import{_ as e,t as U,a5 as m,g as B,b as D,ae as b,ab as V,T as X,ad as Z,al as ee,A as R}from"./index-DJ6xdB0r.js";import{P as te}from"./Panel-DigZB9Yd.js";import{L as ne}from"./ListDetail-DXd5rB2F.js";import"./monaco-editor-BW5C4Iv1.js";function y({children:t,onClick:o,disabled:x,variant:g="default",tooltip:i,type:f="button",className:u}){const v=`px-2 py-0.5 text-xs rounded border transition-colors disabled:opacity-40 disabled:cursor-not-allowed ${g==="primary"?"text-accent border-accent/50 hover:bg-bg-hi":g==="danger"?"text-red-400 hover:text-red-300 border-line hover:border-red-400/50":"text-fg-dim border-line hover:text-fg hover:bg-bg-hi"} ${u??""}`,a=e.jsx("button",{type:f,title:i,onClick:o,disabled:x,className:v,children:t});return i?e.jsx(U,{content:i,align:"bottom-center",children:a}):a}const ae=["inherit","haiku","sonnet","opus"],I=["Read","Grep","Glob","Bash","Write","Edit","WebFetch","WebSearch","Task"],se=["","red","orange","yellow","green","cyan","blue","purple","pink"],z={red:"#c0392b",orange:"#b85c34",yellow:"#c9a227",green:"#6f7d52",cyan:"#3f8f8a",blue:"#4a6fa5",purple:"#7a6a8a",pink:"#b45a80"};function T(t){return{name:t.name,description:t.description??"",tools:t.tools,model:t.model??"inherit",color:t.color??"",tags:t.tags,projects:t.projects??[],action:t.action??"",actionLabel:t.actionLabel??"",body:t.body}}const _={name:"new-agent",description:"",tools:["Read","Grep"],model:"inherit",color:"",tags:[],projects:[],action:"",actionLabel:"",body:""};function re(){const[t,o]=m.useState(null),[x,g]=m.useState(null),[i,f]=m.useState(""),[u,c]=m.useState(null),[v,a]=m.useState(!1),[j,N]=m.useState(!1),[O,C]=m.useState(null),[k,s]=m.useState(!1),w=m.useRef(!0);m.useEffect(()=>()=>{w.current=!1},[]);const h=async n=>{try{const d=await window.api.agents.listPersonas();if(!w.current)return;o(d),g(L=>{var P;return n??L??((P=d[0])==null?void 0:P.name)??null})}catch(d){if(!w.current)return;o([]),b.error(d.message||"failed to load agent personas")}};if(m.useEffect(()=>{h()},[]),m.useEffect(()=>window.api.agents.onChanged(()=>h()),[]),m.useEffect(()=>{const n=V();n&&g(n);const d=L=>g(L.detail);return window.addEventListener("sm:select-persona",d),()=>window.removeEventListener("sm:select-persona",d)},[]),t===null)return e.jsx(B,{title:"loading…"});const $=i.toLowerCase(),E=t.filter(n=>!$||n.name.toLowerCase().includes($)||(n.description??"").toLowerCase().includes($)),l=t.find(n=>n.name===x)??t[0]??null,r=u??(l?T(l):null),F=!!u&&!!l&&JSON.stringify(u)!==JSON.stringify(T(l)),S=v,G=n=>{c(null),a(!1),N(!1),C(null),g(n)},M=n=>c(d=>({...d??(l?T(l):_),...n})),W=()=>{c({..._}),a(!0),N(!1)},J=()=>{r&&(c({...r,name:r.name+"-copy"}),a(!0))},K=()=>{c(null),a(!1)},H=async()=>{if(r){if(!/^[a-z][a-z0-9-]*$/.test(r.name)){b.error('agent name must be lowercase, hyphenated (e.g. "my-agent")');return}s(!0);try{const n={name:r.name,originalName:S||l==null?void 0:l.name,description:r.description,tools:r.tools,model:r.model,color:r.color,tags:r.tags,projects:r.projects,action:r.action,actionLabel:r.actionLabel,body:r.body};await window.api.agents.savePersona(n),b.info(S?`created ${r.name}`:`saved ${r.name}`),c(null),a(!1),await h(r.name)}catch(n){b.error(n.message||"failed to save agent persona")}finally{s(!1)}}},Y=async()=>{if(l){s(!0);try{await window.api.agents.deletePersona({name:l.name}),b.info(`deleted ${l.name}`),N(!1),c(null),a(!1),await h()}catch(n){b.error(n.message||"failed to delete agent persona")}finally{s(!1)}}},q=async n=>{s(!0);try{await window.api.agents.deletePersona({name:n}),b.info(`deleted ${n}`),C(null),x===n&&(c(null),a(!1)),await h()}catch(d){b.error(d.message||"failed to delete agent persona")}finally{s(!1)}},Q=async n=>{if(l)try{await window.api.agents.removeOverride({name:l.name,projectName:n}),b.info(`removed ${n}'s override of ${l.name}`),await h(l.name)}catch(d){b.error(d.message||"failed to remove override")}};return e.jsx(te,{toolbar:e.jsxs(e.Fragment,{children:[e.jsxs("span",{className:"text-fg-faint",children:[t.length," persona",t.length===1?"":"s"]}),e.jsx("input",{value:i,onChange:n=>f(n.target.value),placeholder:"filter…",className:"bg-bg border border-line rounded px-2 py-0.5 text-xs text-fg w-40"}),e.jsx("div",{className:"flex-1"}),e.jsx(y,{variant:"primary",onClick:W,children:"+ New agent"})]}),children:e.jsx(ne,{sidebar:e.jsxs("div",{className:"py-1",children:[S&&e.jsx("div",{className:"w-full text-left px-3 py-1 text-xs flex flex-col gap-0.5 bg-bg-hi text-fg border-l-2 border-accent",children:e.jsxs("span",{className:"flex items-center gap-2 justify-between w-full",children:[e.jsx("span",{className:"truncate font-mono",children:r==null?void 0:r.name}),e.jsx("span",{className:"shrink-0 text-[10px] font-semibold text-accent",children:"new"})]})}),E.length===0&&!S?e.jsx("div",{className:"px-3 py-1 text-xs text-fg-faint italic",children:"no agent personas found"}):E.map(n=>{const d=!S&&n.name===x,L=d&&F,P=O===n.name;return e.jsxs("div",{"data-agent-row":n.name,className:`group w-full text-left px-3 py-1 text-xs flex flex-col gap-0.5 border-l-2 cursor-pointer ${d?"bg-bg-hi text-fg border-accent":"text-fg-dim hover:text-fg hover:bg-bg-hi border-transparent"}`,onClick:()=>G(n.name),children:[e.jsxs("span",{className:"flex items-center gap-2 justify-between w-full",children:[e.jsxs("span",{className:"flex items-center gap-1.5 min-w-0",children:[e.jsx("span",{className:"w-2 h-2 rounded-sm shrink-0",style:{background:n.color?z[n.color]??n.color:"var(--fg-faint, #999)",opacity:n.color?1:.35}}),e.jsx("span",{className:"truncate font-mono",children:n.name})]}),e.jsxs("span",{className:"shrink-0 flex items-center gap-1.5",children:[L&&e.jsx("span",{className:"text-[10px] font-semibold text-accent",children:"unsaved"}),!L&&n.overridingProjects.length>0&&e.jsxs(D,{tone:"dim",children:[n.overridingProjects.length," override",n.overridingProjects.length>1?"s":""]}),P?e.jsxs("span",{className:"flex items-center gap-1",onClick:A=>A.stopPropagation(),children:[e.jsx("button",{onClick:()=>q(n.name),disabled:k,title:`Delete ${n.name}`,className:"text-[10px] font-semibold text-red-400 hover:text-red-300",children:"confirm"}),e.jsx("button",{onClick:()=>C(null),title:"Cancel",className:"text-[10px] text-fg-faint hover:text-fg",children:"cancel"})]}):e.jsx("button",{onClick:A=>{A.stopPropagation(),C(n.name)},title:`Delete ${n.name}`,className:"opacity-0 group-hover:opacity-100 text-fg-faint hover:text-red-400 transition-opacity",children:"×"})]})]}),n.description&&e.jsx("span",{className:"truncate text-fg-faint",children:n.description})]},n.name)})]}),detail:r?e.jsx(oe,{obj:r,set:M,saved:l,dirty:F,isNew:S,busy:k,confirmDelete:j,setConfirmDelete:N,onDuplicate:J,onRevert:K,onSave:H,onDelete:Y,onDropOverride:Q}):e.jsx(B,{title:"select an agent"})})})}const fe=m.memo(re);function oe({obj:t,set:o,saved:x,dirty:g,isNew:i,busy:f,confirmDelete:u,setConfirmDelete:c,onDuplicate:v,onRevert:a,onSave:j,onDelete:N,onDropOverride:O}){const C=`~/.claude/agents/${t.name}.md`,k=g||i;return e.jsxs("div",{className:"max-w-3xl",children:[e.jsxs("div",{className:"px-4 py-3 border-b border-line flex items-center gap-2 flex-wrap",children:[e.jsx("span",{className:"text-sm font-semibold text-fg font-mono",children:t.name}),i?e.jsx(D,{tone:"accent",children:"unsaved draft"}):g?e.jsx(D,{tone:"warn",children:"edited"}):e.jsx(D,{tone:"good",children:"saved"}),e.jsx("span",{className:"ml-auto text-[11px] text-fg-faint font-mono",children:C})]}),e.jsxs("div",{className:"p-4 space-y-4",children:[e.jsx(p,{label:"name",hint:"Filename on disk — lowercase, hyphenated.",children:e.jsx("input",{value:t.name,onChange:s=>o({name:s.target.value.toLowerCase()}),className:"w-full bg-bg border border-line rounded px-2 py-1 text-xs text-fg font-mono"})}),e.jsx(p,{label:"description",hint:"Shown in the list and in agent pickers.",children:e.jsx("input",{value:t.description,onChange:s=>o({description:s.target.value}),className:"w-full bg-bg border border-line rounded px-2 py-1 text-xs text-fg"})}),e.jsx(p,{label:"default model",hint:"This persona's default model for its Epics — sets the --model an Epic launches with (Terminal + Chat both honor it). 'inherit' falls back to each view's own default, not this Settings page. Cheap models for read-only personas; opus for judgement calls.",children:e.jsx(le,{options:ae,value:t.model,onChange:s=>o({model:s}),mono:!0})}),e.jsx(p,{label:"tools",hint:`${t.tools.length} of ${I.length} granted. Anything not ticked is refused at call time.`,children:e.jsx("div",{className:"flex flex-wrap gap-1.5",children:I.map(s=>{const w=t.tools.includes(s);return e.jsx("button",{onClick:()=>o({tools:w?t.tools.filter(h=>h!==s):[...t.tools,s]}),className:`px-2 py-0.5 rounded-full text-[11px] font-mono border ${w?"bg-sage/15 text-sage-dark border-sage/30":"bg-bg-hi text-fg-faint border-line"}`,children:s},s)})})}),e.jsx(p,{label:"color",children:e.jsx("div",{className:"flex gap-1.5",children:se.map(s=>e.jsx("button",{title:s||"none",onClick:()=>o({color:s}),className:"w-5 h-5 rounded",style:{background:s?z[s]:"transparent",border:t.color===s?"2px solid var(--accent, #b85c34)":"1px solid var(--line, #555)"}},s||"none"))})}),e.jsx(p,{label:"tags",hint:"Session mission tags this persona is associated with — shown as the default agent on that tag's own page. The first one (in Tag Library order) is the MISSION an Action button's session opens with.",children:e.jsx("div",{className:"flex flex-wrap gap-1.5",children:X.map(s=>{const w=t.tags.includes(s.tag),h=Z(s.tag);return e.jsxs("button",{onClick:()=>o({tags:w?t.tags.filter($=>$!==s.tag):[...t.tags,s.tag]}),className:`px-2 py-0.5 rounded-full text-[11px] font-mono border ${w?`${h.bg} ${h.text} ${h.border?"border-current/30":"border-transparent"}`:"bg-bg-hi text-fg-faint border-line"}`,children:["#",s.tag]},s.tag)})})}),e.jsx(ie,{obj:t,set:o}),e.jsx(p,{label:"definition",hint:"The system prompt this persona runs under.",children:e.jsx("textarea",{rows:9,value:t.body,onChange:s=>o({body:s.target.value}),className:"w-full bg-bg border border-line rounded px-2 py-1.5 text-xs text-fg font-mono leading-relaxed resize-y"})}),e.jsx(p,{label:"project overrides",hint:"Open projects that shadow this definition with a local copy.",children:x&&x.overridingProjects.length>0?e.jsx("div",{className:"flex flex-wrap gap-1.5",children:x.overridingProjects.map(s=>e.jsxs("span",{className:"inline-flex items-center gap-1.5 bg-bg-elev border border-line rounded px-2 py-0.5 text-[11px] font-mono text-fg-dim",children:[s,e.jsx("button",{onClick:()=>O(s),title:"Drop the local copy",className:"text-fg-faint hover:text-fg",children:"×"})]},s))}):e.jsx("div",{className:"text-xs text-fg-faint italic",children:i?"save first to see project overrides.":"no open project overlays this agent — the global definition applies everywhere."})})]}),e.jsx("div",{className:"px-4 py-2.5 border-t border-line flex items-center gap-2 flex-wrap",children:u?e.jsxs(e.Fragment,{children:[e.jsxs("span",{className:"text-xs text-fg-dim",children:["Delete ",e.jsx("strong",{className:"font-mono",children:t.name})," and its file?"]}),e.jsx(y,{variant:"danger",onClick:N,disabled:f,children:"Delete"}),e.jsx(y,{onClick:()=>c(!1),children:"Keep"})]}):e.jsxs(e.Fragment,{children:[e.jsx(y,{onClick:v,disabled:f,children:"Duplicate"}),!i&&e.jsx(y,{variant:"danger",onClick:()=>c(!0),disabled:f,children:"Delete"}),e.jsx("div",{className:"flex-1"}),k&&e.jsx("span",{className:"text-[11px] text-fg-faint",children:"unsaved changes"}),e.jsx(y,{onClick:a,disabled:!k||f,children:"Revert"}),e.jsx(y,{variant:"primary",onClick:j,disabled:!k||f,children:f?"Saving…":i?"Create":"Save"})]})})]})}function ie({obj:t,set:o}){const{projects:x}=ee(),g=x.map(a=>({cwd:a.cwd,name:a.name})),i=new Set(g.map(a=>a.cwd)),f=t.projects.filter(a=>a!==R&&!i.has(a)),u=t.projects.includes(R),c=a=>o({projects:t.projects.includes(a)?t.projects.filter(j=>j!==a):[...t.projects,a]}),v=t.projects.length>0;return e.jsxs(e.Fragment,{children:[e.jsx(p,{label:"action — appears in",hint:"Projects whose Sessions toolbar gets a one-click button for this agent. Pressing it starts a normal session: this persona is the ACTOR, its first tag is the MISSION, and the action text below is the opening goal. Leave empty for an agent that is only picked by hand in the New Session card.",children:e.jsxs("div",{className:"flex flex-wrap gap-1.5",children:[e.jsx("button",{onClick:()=>c(R),className:`px-2 py-0.5 rounded-full text-[11px] font-mono border ${u?"bg-accent/15 text-accent border-accent/40 font-semibold":"bg-bg-hi text-fg-faint border-line"}`,children:"every project"}),g.map(a=>{const j=t.projects.includes(a.cwd);return e.jsx("button",{title:a.cwd,onClick:()=>c(a.cwd),className:`px-2 py-0.5 rounded-full text-[11px] font-mono border ${j?"bg-sage/15 text-sage-dark border-sage/30":"bg-bg-hi text-fg-faint border-line"} ${u&&!j?"opacity-40":""}`,children:a.name},a.cwd)}),f.map(a=>e.jsxs("button",{title:`${a} — not a known project on this machine`,onClick:()=>c(a),className:"px-2 py-0.5 rounded-full text-[11px] font-mono border bg-bg-hi text-fg-faint border-dashed border-line",children:[a," ×"]},a))]})}),v&&e.jsxs(e.Fragment,{children:[e.jsx(p,{label:"action — button label",hint:"Caption on the Sessions toolbar button. Defaults to the agent name.",children:e.jsx("input",{value:t.actionLabel,onChange:a=>o({actionLabel:a.target.value}),placeholder:t.name,className:"w-full bg-bg border border-line rounded px-2 py-1 text-xs text-fg"})}),e.jsx(p,{label:"action — opening instruction",hint:"Sent as the session's first message, under the AIM framing. Falls back to this persona's description when empty; with neither, no button is shown.",children:e.jsx("textarea",{rows:3,value:t.action,onChange:a=>o({action:a.target.value}),placeholder:t.description||`e.g. /builder
|
|
2
|
-
|
|
3
|
-
Check git vs the published package and publish anything new.`,className:"w-full bg-bg border border-line rounded px-2 py-1.5 text-xs text-fg font-mono leading-relaxed resize-y"})})]})]})}function p({label:t,hint:o,children:x}){return e.jsxs("div",{children:[e.jsx("div",{className:"text-[10.5px] font-semibold uppercase tracking-wide text-fg-faint mb-1.5 font-mono",children:t}),x,o&&e.jsx("div",{className:"text-[11px] text-fg-faint mt-1 leading-snug",children:o})]})}function le({options:t,value:o,onChange:x,mono:g}){return e.jsx("div",{className:"flex flex-wrap gap-1",children:t.map(i=>{const f=i===o;return e.jsx("button",{onClick:()=>x(i),className:`px-2.5 py-1 rounded text-xs border ${g?"font-mono":""} ${f?"bg-accent/15 text-accent border-accent/40 font-semibold":"bg-bg-hi text-fg-dim border-line"}`,children:i},i)})})}export{fe as AgentLibrary};
|