claude-code-session-manager 0.73.2 → 0.75.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 (124) hide show
  1. package/dist/assets/AgentLibrary-SpzptNyq.js +3 -0
  2. package/dist/assets/DataModel-C4M9xVLC.js +1 -0
  3. package/dist/assets/{History-Dnx86P4n.js → History-92FYrZKS.js} +2 -2
  4. package/dist/assets/{Hooks-CAYlpqBp.js → Hooks-DlnavO1l.js} +2 -2
  5. package/dist/assets/HostBilko-CKI7jt1z.js +1 -0
  6. package/dist/assets/{Library-DVbXlboP.js → Library-Brhxe7xa.js} +1 -1
  7. package/dist/assets/ListDetail-DmIweYTN.js +1 -0
  8. package/dist/assets/MarkdownEditor-DA4xqWWi.js +1 -0
  9. package/dist/assets/{McpServers-BxUk1aR3.js → McpServers-DWXQGzVc.js} +2 -2
  10. package/dist/assets/{Memory-DVbPQFZw.js → Memory-HCx9Arao.js} +4 -4
  11. package/dist/assets/Panel-B20C6kN8.js +1 -0
  12. package/dist/assets/{Permissions-CSLxAoTL.js → Permissions-C4sPgLpA.js} +3 -3
  13. package/dist/assets/Plugins-CaRenCBg.js +2 -0
  14. package/dist/assets/ProvenanceBadge-DfrXzTeh.js +1 -0
  15. package/dist/assets/SaveBar-BmpUx2Yh.js +1 -0
  16. package/dist/assets/{Scheduler-CrBVVXN1.js → Scheduler-xUD8pQ6q.js} +7 -7
  17. package/dist/assets/ScopeSwitcher-DVlKMck5.js +1 -0
  18. package/dist/assets/Settings-C457mxOK.js +3 -0
  19. package/dist/assets/{SkillReferenceGraph-B1DaeT2R.js → SkillReferenceGraph-D3dDc5X7.js} +1 -1
  20. package/dist/assets/{Skills-CSdxEC-f.js → Skills-BNDlbftC.js} +2 -2
  21. package/dist/assets/SystemPrompt-d-ykNRec.js +1 -0
  22. package/dist/assets/TagLibrary-D3JWkQK-.js +1 -0
  23. package/dist/assets/{TiptapBody-4hzD0jjv.js → TiptapBody-B1Ox6D6E.js} +1 -1
  24. package/dist/assets/Toggle-CaYw6Lrj.js +1 -0
  25. package/dist/assets/{index-DJ6xdB0r.js → index-CEeLxNCz.js} +659 -655
  26. package/dist/assets/{index-D9C8N3pn.css → index-Dbo9R4e0.css} +1 -1
  27. package/dist/assets/{settingsSchema-DVblsXFp.js → settingsSchema-DUkd45M5.js} +1 -1
  28. package/dist/index.html +2 -2
  29. package/package.json +3 -1
  30. package/plugins/session-manager-dev/skills/memory-sanitation/SKILL.md +21 -1
  31. package/scripts/scheduler-mcp-server.cjs +471 -0
  32. package/src/main/__tests__/agentModelResolve.test.cjs +19 -0
  33. package/src/main/__tests__/crossProjectFeedback.test.cjs +7 -0
  34. package/src/main/__tests__/crossProjectFeedbackRoutes.test.cjs +7 -0
  35. package/src/main/__tests__/epicMint.test.cjs +50 -2
  36. package/src/main/__tests__/health-claude-md-budget.test.cjs +57 -0
  37. package/src/main/__tests__/prdCreate.test.cjs +17 -0
  38. package/src/main/__tests__/promptSessionsCreateEpicHandler.test.cjs +7 -0
  39. package/src/main/__tests__/seedAgentPersonas.test.cjs +105 -0
  40. package/src/main/__tests__/seedSchedulerMcp.test.cjs +145 -0
  41. package/src/main/__tests__/seedStatus.test.cjs +100 -0
  42. package/src/main/__tests__/workTypeLibrary.test.cjs +89 -0
  43. package/src/main/agentLibrary.cjs +43 -0
  44. package/src/main/health.cjs +51 -1
  45. package/src/main/index.cjs +44 -3
  46. package/src/main/ipcSchemas.cjs +46 -9
  47. package/src/main/lib/__tests__/agentPersonaSchema.test.cjs +67 -0
  48. package/src/main/lib/__tests__/delegationReadiness.test.cjs +185 -0
  49. package/src/main/lib/__tests__/epicDelegationStats.test.cjs +137 -0
  50. package/src/main/lib/agentModelResolve.cjs +32 -1
  51. package/src/main/lib/agentPersonaSchema.cjs +82 -0
  52. package/src/main/lib/delegationReadiness.cjs +141 -0
  53. package/src/main/lib/epicDelegationStats.cjs +124 -0
  54. package/src/main/lib/epicMint.cjs +74 -1
  55. package/src/main/lib/prdFrontmatter.cjs +6 -5
  56. package/src/main/lib/promptSessionSchema.cjs +7 -15
  57. package/src/main/lib/workTypeLibrary.cjs +48 -0
  58. package/src/main/seedAgentPersonas.cjs +92 -0
  59. package/src/main/seedDevPlugin.cjs +14 -2
  60. package/src/main/seedSchedulerMcp.cjs +148 -0
  61. package/src/main/seedStatus.cjs +59 -0
  62. package/src/preload/api.d.ts +47 -3
  63. package/src/preload/index.cjs +7 -0
  64. package/src/seed/agents/architect.md +64 -0
  65. package/src/seed/agents/dev-lead.md +42 -0
  66. package/src/seed/prompts/code-review/ac-coverage-check.md +8 -0
  67. package/src/seed/prompts/code-review/correctness-only.md +8 -0
  68. package/src/seed/prompts/code-review/full-spectrum-high.md +8 -0
  69. package/src/seed/prompts/code-review/hallucination-check.md +8 -0
  70. package/src/seed/prompts/code-review/public-api-compat.md +8 -0
  71. package/src/seed/prompts/code-review/readability-naming.md +8 -0
  72. package/src/seed/prompts/debugging/bug-as-failing-test.md +8 -0
  73. package/src/seed/prompts/debugging/git-bisect-regression.md +8 -0
  74. package/src/seed/prompts/debugging/instrument-intermittent-bug.md +8 -0
  75. package/src/seed/prompts/debugging/localize-pipeline-failure.md +8 -0
  76. package/src/seed/prompts/debugging/reproduce-then-diagnose.md +8 -0
  77. package/src/seed/prompts/documentation/adr-from-change.md +8 -0
  78. package/src/seed/prompts/documentation/module-readme.md +8 -0
  79. package/src/seed/prompts/documentation/onboarding-plan.md +8 -0
  80. package/src/seed/prompts/documentation/refresh-claude-md.md +8 -0
  81. package/src/seed/prompts/documentation/tsdoc-public-exports.md +8 -0
  82. package/src/seed/prompts/git-pr/conventional-commit.md +8 -0
  83. package/src/seed/prompts/git-pr/draft-pr-title-body.md +8 -0
  84. package/src/seed/prompts/git-pr/pre-commit-safety-sweep.md +8 -0
  85. package/src/seed/prompts/git-pr/release-notes-block.md +8 -0
  86. package/src/seed/prompts/git-pr/split-large-pr.md +8 -0
  87. package/src/seed/prompts/performance/bundle-startup-audit.md +8 -0
  88. package/src/seed/prompts/performance/complexity-audit.md +8 -0
  89. package/src/seed/prompts/performance/cpu-profile-hot-path.md +8 -0
  90. package/src/seed/prompts/performance/db-query-plan-review.md +8 -0
  91. package/src/seed/prompts/performance/memory-leak-hunt.md +8 -0
  92. package/src/seed/prompts/qa/api-contract-tests.md +8 -0
  93. package/src/seed/prompts/qa/e2e-critical-path.md +8 -0
  94. package/src/seed/prompts/qa/failing-test-for-bug.md +8 -0
  95. package/src/seed/prompts/qa/find-missing-test-coverage.md +8 -0
  96. package/src/seed/prompts/qa/stabilize-flaky-test.md +8 -0
  97. package/src/seed/prompts/qa/tdd-red-first.md +8 -0
  98. package/src/seed/prompts/qa/visual-regression-review.md +8 -0
  99. package/src/seed/prompts/qa/wcag-axe-scan.md +8 -0
  100. package/src/seed/prompts/refactoring/dead-code-sweep.md +8 -0
  101. package/src/seed/prompts/refactoring/extract-duplicated-pattern.md +8 -0
  102. package/src/seed/prompts/refactoring/modernize-legacy-file.md +8 -0
  103. package/src/seed/prompts/refactoring/reduce-cyclomatic-complexity.md +8 -0
  104. package/src/seed/prompts/refactoring/tighten-module-boundaries.md +8 -0
  105. package/src/seed/prompts/security/authz-audit.md +8 -0
  106. package/src/seed/prompts/security/crypto-correctness.md +8 -0
  107. package/src/seed/prompts/security/cwe-top-25-hunt.md +8 -0
  108. package/src/seed/prompts/security/dependency-audit.md +8 -0
  109. package/src/seed/prompts/security/ipc-boundary-hardening.md +8 -0
  110. package/src/seed/prompts/security/owasp-top-10-staged-diff.md +8 -0
  111. package/src/seed/prompts/security/secret-credential-scan.md +8 -0
  112. package/dist/assets/AgentLibrary-D0frFrHG.js +0 -3
  113. package/dist/assets/HostBilko-CXykS2ap.js +0 -1
  114. package/dist/assets/ListDetail-DXd5rB2F.js +0 -1
  115. package/dist/assets/MarkdownEditor-D0mO1TUZ.js +0 -1
  116. package/dist/assets/Panel-DigZB9Yd.js +0 -1
  117. package/dist/assets/Plugins-DOTiP9sQ.js +0 -2
  118. package/dist/assets/ProvenanceBadge-PsNd-CBq.js +0 -1
  119. package/dist/assets/SaveBar-C4bgDklX.js +0 -1
  120. package/dist/assets/ScopeSwitcher-RMAwgmj4.js +0 -1
  121. package/dist/assets/Settings-CM4lZxnn.js +0 -3
  122. package/dist/assets/SystemPrompt-j-WYOC5f.js +0 -1
  123. package/dist/assets/TagLibrary-Fj0zrPru.js +0 -1
  124. package/dist/assets/Toggle-H6pNcv3l.js +0 -1
@@ -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
+ };
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Delegation-readiness probe — answers "can this project actually hand work
3
+ * to the scheduler?" as structured data instead of a silent tool-list gap.
4
+ *
5
+ * Why this exists: when the session-manager-scheduler MCP server isn't
6
+ * registered, `scheduler_create_prd` is simply absent from the agent's tool
7
+ * list — there's no error to catch, so an agent asked to delegate just
8
+ * implements inline instead. See scripts/install-scheduler-mcp-user-scope.sh's
9
+ * header for the incident this repeat-guards against (PRD 1024-1030).
10
+ *
11
+ * This module only computes the four checks below; surfacing them in the UI
12
+ * or the Epic's grounding line is a dependent PRD (delegation-readiness-ui).
13
+ */
14
+
15
+ const fs = require('node:fs');
16
+ const path = require('node:path');
17
+ const os = require('node:os');
18
+
19
+ const SCHEDULER_MCP_NAME = 'session-manager-scheduler';
20
+ const DEV_PLUGIN_ENABLED_KEY = 'session-manager-dev@session-manager';
21
+ const SCHEDULER_MCP_SERVER_SCRIPT = path.resolve(__dirname, '..', '..', '..', 'scripts', 'scheduler-mcp-server.cjs');
22
+
23
+ /** Read + JSON.parse a file, returning `fallback` on any read/parse failure. */
24
+ function readJsonSafe(absPath, fallback) {
25
+ try {
26
+ return JSON.parse(fs.readFileSync(absPath, 'utf8'));
27
+ } catch {
28
+ return fallback;
29
+ }
30
+ }
31
+
32
+ function checkSchedulerMcp({ cwd, homeDir }) {
33
+ const userConfig = readJsonSafe(path.join(homeDir, '.claude.json'), null);
34
+ const userHas = !!userConfig?.mcpServers?.[SCHEDULER_MCP_NAME];
35
+
36
+ const projectConfig = readJsonSafe(path.join(cwd, '.mcp.json'), null);
37
+ const projectHas = !!projectConfig?.mcpServers?.[SCHEDULER_MCP_NAME];
38
+
39
+ const ok = userHas || projectHas;
40
+ let detail;
41
+ if (userHas) detail = `${SCHEDULER_MCP_NAME} registered at user scope (~/.claude.json)`;
42
+ else if (projectHas) detail = `${SCHEDULER_MCP_NAME} registered at project scope (${cwd}/.mcp.json)`;
43
+ else detail = `no ${SCHEDULER_MCP_NAME} entry in ~/.claude.json or ${cwd}/.mcp.json`;
44
+
45
+ return {
46
+ id: 'scheduler-mcp',
47
+ label: 'Scheduler MCP server registered',
48
+ ok,
49
+ detail,
50
+ fix: ok
51
+ ? null
52
+ : `claude mcp add ${SCHEDULER_MCP_NAME} --scope user -- node ${SCHEDULER_MCP_SERVER_SCRIPT}`,
53
+ };
54
+ }
55
+
56
+ function checkDevPlugin({ homeDir }) {
57
+ const settings = readJsonSafe(path.join(homeDir, '.claude', 'settings.json'), null);
58
+ const ok = settings?.enabledPlugins?.[DEV_PLUGIN_ENABLED_KEY] === true;
59
+
60
+ return {
61
+ id: 'dev-plugin',
62
+ label: 'session-manager-dev plugin enabled',
63
+ ok,
64
+ detail: ok
65
+ ? `${DEV_PLUGIN_ENABLED_KEY} enabled in ~/.claude/settings.json`
66
+ : `${DEV_PLUGIN_ENABLED_KEY} missing from ~/.claude/settings.json enabledPlugins`,
67
+ fix: ok ? null : 'Enable in Plugins tab: session-manager-dev (Session Manager)',
68
+ };
69
+ }
70
+
71
+ // Personas resolve from BOTH scopes (agentLibrary.cjs: "project cwd wins
72
+ // over the global" one) — a project that only ever defines project-scoped
73
+ // overrides in `<cwd>/.claude/agents/` (this repo's own `.claude/agents/`
74
+ // is exactly that case) is a legitimately delegation-ready project, not a
75
+ // failing one, so both directories must be counted.
76
+ function countMdFiles(dir) {
77
+ try {
78
+ return fs.readdirSync(dir).filter((f) => f.endsWith('.md')).length;
79
+ } catch {
80
+ return 0;
81
+ }
82
+ }
83
+
84
+ function checkAgentPersonas({ cwd, homeDir }) {
85
+ const globalCount = countMdFiles(path.join(homeDir, '.claude', 'agents'));
86
+ const projectCount = countMdFiles(path.join(cwd, '.claude', 'agents'));
87
+ const count = globalCount + projectCount;
88
+ const ok = count > 0;
89
+
90
+ return {
91
+ id: 'agent-personas',
92
+ label: 'At least one agent persona defined',
93
+ ok,
94
+ detail: ok
95
+ ? `${count} persona(s) across ~/.claude/agents/ and ${cwd}/.claude/agents/`
96
+ : `no .md personas in ~/.claude/agents/ or ${cwd}/.claude/agents/`,
97
+ fix: ok ? null : 'Create a persona in the Agent Library tab',
98
+ };
99
+ }
100
+
101
+ function checkPrdWriteGuard({ cwd }) {
102
+ const settings = readJsonSafe(path.join(cwd, '.claude', 'settings.json'), null);
103
+ const preToolUse = Array.isArray(settings?.hooks?.PreToolUse) ? settings.hooks.PreToolUse : [];
104
+ const ok = preToolUse.some((matcher) =>
105
+ Array.isArray(matcher?.hooks) &&
106
+ matcher.hooks.some((h) => typeof h?.command === 'string' && h.command.includes('guard-prd-writes'))
107
+ );
108
+
109
+ return {
110
+ id: 'prd-write-guard',
111
+ label: 'PRD-write guard hook installed',
112
+ ok,
113
+ detail: ok
114
+ ? `guard-prd-writes PreToolUse hook found in ${cwd}/.claude/settings.json`
115
+ : `no guard-prd-writes PreToolUse hook in ${cwd}/.claude/settings.json`,
116
+ fix: ok
117
+ ? null
118
+ : `Add a PreToolUse hook to ${cwd}/.claude/settings.json matching Write|Edit|NotebookEdit that runs node scripts/hooks/guard-prd-writes.cjs`,
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Runs all four delegation-readiness checks for `cwd`. Every filesystem read
124
+ * is wrapped (readJsonSafe / try-catch) so a missing or unparseable file
125
+ * yields ok:false with a detail, never a thrown exception.
126
+ */
127
+ function checkDelegationReadiness({ cwd, homeDir = os.homedir() }) {
128
+ const checks = [
129
+ checkSchedulerMcp({ cwd, homeDir }),
130
+ checkDevPlugin({ homeDir }),
131
+ checkAgentPersonas({ cwd, homeDir }),
132
+ checkPrdWriteGuard({ cwd }),
133
+ ];
134
+
135
+ return {
136
+ ok: checks.every((c) => c.ok),
137
+ checks,
138
+ };
139
+ }
140
+
141
+ module.exports = { checkDelegationReadiness };
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Derived-at-read-time counters answering "did this Epic delegate work into
5
+ * PRDs, or did the interactive session just edit application source
6
+ * inline?" — no new writer, no new namespace; both numbers are computed
7
+ * from state that already exists on disk (single-writer law,
8
+ * opsOwnership.cjs).
9
+ *
10
+ * prdsQueued reads the Epic's own live PRD dir
11
+ * (session-manager-operations/scheduler/epics/<epicId>/prds/) — the
12
+ * "prds-archived" sibling deliberately does NOT count here: this is a
13
+ * live-delegation signal, not a lifetime PRD count (that's epicPrds() in
14
+ * the renderer, sourced from the scheduler's own snapshot).
15
+ *
16
+ * inlineEdits reads the Epic's OWN interactive claude session transcript —
17
+ * ~/.claude/projects/<encodeCwd(cwd)>/<claudeSessionId>.jsonl, the same file
18
+ * transcripts.cjs already tails for the live feed — and counts Write/Edit/
19
+ * NotebookEdit tool_use blocks whose file_path resolves under this repo's
20
+ * src/, scripts/, plugins/, or bin/. That transcript already carries real
21
+ * tool_use blocks (classifyTranscriptLine.cjs); the durable
22
+ * session-manager-operations/prompt-sessions/transcripts/<epicId>.jsonl
23
+ * store does NOT (it holds only rendered {role, text} turns, verified by
24
+ * reading one), so it is not a usable source for this counter.
25
+ */
26
+
27
+ const fs = require('node:fs');
28
+ const os = require('node:os');
29
+ const path = require('node:path');
30
+ const { encodeCwd } = require('./encodeCwd.cjs');
31
+ const { classifyLine } = require('./classifyTranscriptLine.cjs');
32
+
33
+ // Same cap transcripts.cjs's readDelta uses for a single pass — bounds
34
+ // memory/time on a very large transcript instead of reading it whole.
35
+ const MAX_TRANSCRIPT_BYTES = 8 * 1024 * 1024;
36
+
37
+ const INLINE_EDIT_TOOL_NAMES = new Set(['Write', 'Edit', 'NotebookEdit']);
38
+ const APPLICATION_SOURCE_PREFIXES = ['src/', 'scripts/', 'plugins/', 'bin/'];
39
+
40
+ function countPrdsQueued(cwd, epicId, deps = {}) {
41
+ const readdirSync = deps.readdirSync || fs.readdirSync;
42
+ try {
43
+ const dir = path.join(cwd, 'session-manager-operations', 'scheduler', 'epics', epicId, 'prds');
44
+ return readdirSync(dir).filter((name) => name.endsWith('.md')).length;
45
+ } catch {
46
+ return 0;
47
+ }
48
+ }
49
+
50
+ /** True when `filePath` (absolute or relative) resolves to a path under
51
+ * cwd's src/, scripts/, plugins/, or bin/ — never a dotfile/root config,
52
+ * never a path outside the repo entirely. */
53
+ function isApplicationSourcePath(cwd, filePath) {
54
+ if (typeof filePath !== 'string' || !filePath) return false;
55
+ const rel = path.relative(cwd, path.resolve(cwd, filePath)).split(path.sep).join('/');
56
+ if (rel.startsWith('..')) return false;
57
+ return APPLICATION_SOURCE_PREFIXES.some((prefix) => rel === prefix.slice(0, -1) || rel.startsWith(prefix));
58
+ }
59
+
60
+ /** Reads at most the trailing MAX_TRANSCRIPT_BYTES of `filePath`, dropping
61
+ * a truncated leading partial line when the read didn't start at byte 0. */
62
+ function readTranscriptTail(filePath, deps = {}) {
63
+ const statSync = deps.statSync || fs.statSync;
64
+ const openSync = deps.openSync || fs.openSync;
65
+ const readSync = deps.readSync || fs.readSync;
66
+ const closeSync = deps.closeSync || fs.closeSync;
67
+
68
+ const size = statSync(filePath).size;
69
+ const start = Math.max(0, size - MAX_TRANSCRIPT_BYTES);
70
+ const length = size - start;
71
+ const buffer = Buffer.alloc(length);
72
+ const fd = openSync(filePath, 'r');
73
+ try {
74
+ readSync(fd, buffer, 0, length, start);
75
+ } finally {
76
+ closeSync(fd);
77
+ }
78
+ const text = buffer.toString('utf8');
79
+ return start > 0 ? text.slice(text.indexOf('\n') + 1) : text;
80
+ }
81
+
82
+ function countInlineEdits(cwd, claudeSessionId, deps = {}) {
83
+ if (!claudeSessionId) return 0;
84
+ const readFn = deps.readTranscriptTail || readTranscriptTail;
85
+ const homeDir = deps.homeDir || os.homedir();
86
+ try {
87
+ const filePath = path.join(homeDir, '.claude', 'projects', encodeCwd(cwd), `${claudeSessionId}.jsonl`);
88
+ const text = readFn(filePath, deps);
89
+ let count = 0;
90
+ for (const line of text.split('\n')) {
91
+ const trimmed = line.trim();
92
+ if (!trimmed) continue;
93
+ let obj;
94
+ try {
95
+ obj = JSON.parse(trimmed);
96
+ } catch {
97
+ continue;
98
+ }
99
+ for (const event of classifyLine(obj)) {
100
+ if (event.kind !== 'tool_use') continue;
101
+ const { name, input } = event.data || {};
102
+ if (!INLINE_EDIT_TOOL_NAMES.has(name)) continue;
103
+ if (isApplicationSourcePath(cwd, input?.file_path)) count += 1;
104
+ }
105
+ }
106
+ return count;
107
+ } catch {
108
+ return 0;
109
+ }
110
+ }
111
+
112
+ function computeEpicDelegationStats(cwd, epicId, claudeSessionId, deps = {}) {
113
+ return {
114
+ prdsQueued: countPrdsQueued(cwd, epicId, deps),
115
+ inlineEdits: countInlineEdits(cwd, claudeSessionId, deps),
116
+ };
117
+ }
118
+
119
+ module.exports = {
120
+ computeEpicDelegationStats,
121
+ countPrdsQueued,
122
+ countInlineEdits,
123
+ isApplicationSourcePath,
124
+ };
@@ -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({
@@ -49,7 +41,7 @@ const EpicSourceSchema = z.object({
49
41
  // data so the Epic's first turn can render a structured AIM briefing card
50
42
  // instead of regex-parsing the flat string back apart.
51
43
  const EpicIntakeSectionSchema = z.object({
52
- kind: z.enum(['actor', 'injection', 'input', 'mission', 'goal', 'reference']),
44
+ kind: z.enum(['actor', 'persona-body', 'injection', 'input', 'mission', 'goal', 'reference']),
53
45
  label: z.string(),
54
46
  text: z.string(),
55
47
  source: z.string().optional(),
@@ -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
+ };
@@ -0,0 +1,92 @@
1
+ /**
2
+ * First-boot seeder for the bundled Agent personas (Architect, Dev Lead).
3
+ *
4
+ * On a fresh install `~/.claude/agents/` is empty, so the Agent Library has
5
+ * nothing to offer — no persona to select for an Epic's Actor. This seeder
6
+ * copies the personas bundled at `src/seed/agents/*.md` into `~/.claude/agents/`
7
+ * on first boot, one file per persona, and NEVER overwrites a file that
8
+ * already exists there (hand-edited or user-authored personas win).
9
+ *
10
+ * Idempotent the same way as seedDevPlugin.cjs:
11
+ * 1. A marker file (`~/.claude/session-manager/.agent-personas-seeded`)
12
+ * records seed state. Once seeding SUCCEEDS we write `done` and never
13
+ * touch it again. A FAILED attempt only bumps an attempt counter and
14
+ * retries on the next few boots (bounded by MAX_ATTEMPTS), so a
15
+ * transient first-boot failure doesn't permanently skip the personas.
16
+ *
17
+ * Fire-and-forget: called post-window from index.cjs. Errors are logged,
18
+ * never thrown. Kill-switch: SM_SEED_AGENT_PERSONAS_DISABLE=1.
19
+ */
20
+
21
+ const fs = require('node:fs');
22
+ const path = require('node:path');
23
+ const os = require('node:os');
24
+ const { writeJsonSync } = require('./config.cjs');
25
+
26
+ const PERSONAS = ['architect', 'dev-lead'];
27
+ const MAX_ATTEMPTS = 3; // give a transient first-boot failure a few chances
28
+
29
+ function bundledSourceDir() {
30
+ return path.join(__dirname, '..', 'seed', 'agents');
31
+ }
32
+
33
+ function destAgentsDir() {
34
+ return path.join(os.homedir(), '.claude', 'agents');
35
+ }
36
+
37
+ function markerPath() {
38
+ return path.join(os.homedir(), '.claude', 'session-manager', '.agent-personas-seeded');
39
+ }
40
+
41
+ /** Read the marker → { done:boolean, attempts:number }. Absent = fresh. */
42
+ function readMarker() {
43
+ try {
44
+ const raw = fs.readFileSync(markerPath(), 'utf8').trim();
45
+ const m = JSON.parse(raw);
46
+ return { done: !!m.done, attempts: Number(m.attempts) || 0 };
47
+ } catch {
48
+ return { done: false, attempts: 0 };
49
+ }
50
+ }
51
+
52
+ function writeMarker(state) {
53
+ try {
54
+ writeJsonSync(markerPath(), { ...state, ts: new Date().toISOString() });
55
+ } catch (err) {
56
+ console.warn('[seedAgentPersonas] could not write marker:', err?.message ?? err);
57
+ }
58
+ }
59
+
60
+ async function seedAgentPersonas({ logger = console, writeLog = () => {} } = {}) {
61
+ if (process.env.SM_SEED_AGENT_PERSONAS_DISABLE === '1') return;
62
+ const marker = readMarker();
63
+ if (marker.done) return; // succeeded before — leave it alone.
64
+ if (marker.attempts >= MAX_ATTEMPTS) return; // gave up — manual copy only.
65
+
66
+ try {
67
+ const srcDir = bundledSourceDir();
68
+ const destDir = destAgentsDir();
69
+ fs.mkdirSync(destDir, { recursive: true });
70
+
71
+ for (const name of PERSONAS) {
72
+ const src = path.join(srcDir, `${name}.md`);
73
+ const dest = path.join(destDir, `${name}.md`);
74
+ if (fs.existsSync(dest)) continue; // never overwrite an existing persona
75
+ fs.copyFileSync(src, dest);
76
+ logger.log?.(`[seedAgentPersonas] seeded ${name}.md`);
77
+ }
78
+
79
+ writeMarker({ done: true, attempts: marker.attempts });
80
+ } catch (err) {
81
+ logger.warn?.('[seedAgentPersonas] error:', err?.message ?? err);
82
+ writeLog({
83
+ scope: 'seed-agent-personas',
84
+ level: 'error',
85
+ message: 'seed error',
86
+ meta: { error: err?.message ?? String(err), attempt: marker.attempts + 1, maxAttempts: MAX_ATTEMPTS },
87
+ });
88
+ writeMarker({ done: false, attempts: marker.attempts + 1 });
89
+ }
90
+ }
91
+
92
+ module.exports = { seedAgentPersonas, markerPath, MAX_ATTEMPTS };