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.
Files changed (49) hide show
  1. package/dist/assets/AgentLibrary-CSnWV6uv.js +3 -0
  2. package/dist/assets/{History-Dnx86P4n.js → History-ShwkUTny.js} +1 -1
  3. package/dist/assets/{Hooks-CAYlpqBp.js → Hooks-Dc5y1rZH.js} +1 -1
  4. package/dist/assets/{HostBilko-CXykS2ap.js → HostBilko-evd1hD8q.js} +1 -1
  5. package/dist/assets/{Library-DVbXlboP.js → Library-BOL6GqFX.js} +1 -1
  6. package/dist/assets/{ListDetail-DXd5rB2F.js → ListDetail-BPJb2et3.js} +1 -1
  7. package/dist/assets/{MarkdownEditor-D0mO1TUZ.js → MarkdownEditor-CACsHVuf.js} +1 -1
  8. package/dist/assets/{McpServers-BxUk1aR3.js → McpServers-DuOTxmc4.js} +1 -1
  9. package/dist/assets/{Memory-DVbPQFZw.js → Memory-BYJHDz7W.js} +1 -1
  10. package/dist/assets/{Panel-DigZB9Yd.js → Panel-n-p8OQLc.js} +1 -1
  11. package/dist/assets/{Permissions-CSLxAoTL.js → Permissions-DhYERDVR.js} +1 -1
  12. package/dist/assets/{Plugins-DOTiP9sQ.js → Plugins-D1ckVgo3.js} +2 -2
  13. package/dist/assets/{ProvenanceBadge-PsNd-CBq.js → ProvenanceBadge-ByTjDwAX.js} +1 -1
  14. package/dist/assets/{SaveBar-C4bgDklX.js → SaveBar-_ORUCmZw.js} +1 -1
  15. package/dist/assets/{Scheduler-CrBVVXN1.js → Scheduler-Bu6kSkl3.js} +7 -7
  16. package/dist/assets/{ScopeSwitcher-RMAwgmj4.js → ScopeSwitcher-Css6Sg7Y.js} +1 -1
  17. package/dist/assets/{Settings-CM4lZxnn.js → Settings-DlyJjexf.js} +1 -1
  18. package/dist/assets/{SkillReferenceGraph-B1DaeT2R.js → SkillReferenceGraph-rKaTHmOg.js} +1 -1
  19. package/dist/assets/{Skills-CSdxEC-f.js → Skills-BHlH2uG5.js} +1 -1
  20. package/dist/assets/{SystemPrompt-j-WYOC5f.js → SystemPrompt-DsgF8xsl.js} +1 -1
  21. package/dist/assets/TagLibrary-Q5QsKMes.js +1 -0
  22. package/dist/assets/{TiptapBody-4hzD0jjv.js → TiptapBody-XoQHiqfF.js} +1 -1
  23. package/dist/assets/{Toggle-H6pNcv3l.js → Toggle-BJHhHQOq.js} +1 -1
  24. package/dist/assets/{index-DJ6xdB0r.js → index-Cs_Lq9-w.js} +3 -3
  25. package/dist/assets/{settingsSchema-DVblsXFp.js → settingsSchema-BTDJcu2x.js} +1 -1
  26. package/dist/index.html +1 -1
  27. package/package.json +1 -1
  28. package/plugins/session-manager-dev/skills/memory-sanitation/SKILL.md +21 -1
  29. package/src/main/__tests__/agentModelResolve.test.cjs +19 -0
  30. package/src/main/__tests__/crossProjectFeedback.test.cjs +7 -0
  31. package/src/main/__tests__/crossProjectFeedbackRoutes.test.cjs +7 -0
  32. package/src/main/__tests__/epicMint.test.cjs +50 -2
  33. package/src/main/__tests__/health-claude-md-budget.test.cjs +57 -0
  34. package/src/main/__tests__/prdCreate.test.cjs +17 -0
  35. package/src/main/__tests__/promptSessionsCreateEpicHandler.test.cjs +7 -0
  36. package/src/main/__tests__/workTypeLibrary.test.cjs +89 -0
  37. package/src/main/health.cjs +51 -1
  38. package/src/main/index.cjs +2 -2
  39. package/src/main/ipcSchemas.cjs +21 -9
  40. package/src/main/lib/__tests__/agentPersonaSchema.test.cjs +67 -0
  41. package/src/main/lib/agentModelResolve.cjs +32 -1
  42. package/src/main/lib/agentPersonaSchema.cjs +82 -0
  43. package/src/main/lib/epicMint.cjs +74 -1
  44. package/src/main/lib/prdFrontmatter.cjs +6 -5
  45. package/src/main/lib/promptSessionSchema.cjs +6 -14
  46. package/src/main/lib/workTypeLibrary.cjs +48 -0
  47. package/src/preload/api.d.ts +7 -2
  48. package/dist/assets/AgentLibrary-D0frFrHG.js +0 -3
  49. package/dist/assets/TagLibrary-Fj0zrPru.js +0 -1
@@ -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
  };
@@ -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 (_e, payload) => {
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();
@@ -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 Feature/Bug tag (PRD 774) carried from the originating
335
- // PromptTicket — deterministic, never LLM-classified. NOT the same value
336
- // set as PromptSession's Epic-mission `tag` (lib/promptSessionSchema.cjs's
337
- // EpicTagSchema) — that one also includes 'bilko-host-publisher'. Keep
338
- // these two enums in sync deliberately if their meanings ever converge;
339
- // don't assume editing one updates the other.
340
- tag: z.enum(['feature', 'bug', 'discussion', 'build', 'project-home-builder']).optional(),
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
- tag: z.enum(['feature', 'bug', 'build']).optional(),
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: a 'discussion'-tagged
122
- // ticket never reaches PRD authoring, so that value is never parsed
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, NOT the same value set as ipcSchemas.cjs's scheduleRetagPrd
19
- // tag enum (~line 349), which only covers 5 of these 6 values ('feature',
20
- // 'bug', 'discussion', 'build', 'project-home-builder') and is missing
21
- // 'bilko-host-publisher' — the two enums describe related but distinct
22
- // concepts (PRD-scheduling tag vs. Epic-mission tag), so this is its own
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
+ };
@@ -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: string[];
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: string[];
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};