claude-code-session-manager 0.74.0 → 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 (108) 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-ShwkUTny.js → History-92FYrZKS.js} +2 -2
  4. package/dist/assets/{Hooks-Dc5y1rZH.js → Hooks-DlnavO1l.js} +2 -2
  5. package/dist/assets/HostBilko-CKI7jt1z.js +1 -0
  6. package/dist/assets/{Library-BOL6GqFX.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-DuOTxmc4.js → McpServers-DWXQGzVc.js} +2 -2
  10. package/dist/assets/{Memory-BYJHDz7W.js → Memory-HCx9Arao.js} +4 -4
  11. package/dist/assets/Panel-B20C6kN8.js +1 -0
  12. package/dist/assets/{Permissions-DhYERDVR.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-Bu6kSkl3.js → Scheduler-xUD8pQ6q.js} +6 -6
  17. package/dist/assets/ScopeSwitcher-DVlKMck5.js +1 -0
  18. package/dist/assets/Settings-C457mxOK.js +3 -0
  19. package/dist/assets/{SkillReferenceGraph-rKaTHmOg.js → SkillReferenceGraph-D3dDc5X7.js} +1 -1
  20. package/dist/assets/{Skills-BHlH2uG5.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-XoQHiqfF.js → TiptapBody-B1Ox6D6E.js} +1 -1
  24. package/dist/assets/Toggle-CaYw6Lrj.js +1 -0
  25. package/dist/assets/{index-Cs_Lq9-w.js → index-CEeLxNCz.js} +659 -655
  26. package/dist/assets/{index-D9C8N3pn.css → index-Dbo9R4e0.css} +1 -1
  27. package/dist/assets/{settingsSchema-BTDJcu2x.js → settingsSchema-DUkd45M5.js} +1 -1
  28. package/dist/index.html +2 -2
  29. package/package.json +3 -1
  30. package/scripts/scheduler-mcp-server.cjs +471 -0
  31. package/src/main/__tests__/seedAgentPersonas.test.cjs +105 -0
  32. package/src/main/__tests__/seedSchedulerMcp.test.cjs +145 -0
  33. package/src/main/__tests__/seedStatus.test.cjs +100 -0
  34. package/src/main/agentLibrary.cjs +43 -0
  35. package/src/main/index.cjs +42 -1
  36. package/src/main/ipcSchemas.cjs +25 -0
  37. package/src/main/lib/__tests__/delegationReadiness.test.cjs +185 -0
  38. package/src/main/lib/__tests__/epicDelegationStats.test.cjs +137 -0
  39. package/src/main/lib/delegationReadiness.cjs +141 -0
  40. package/src/main/lib/epicDelegationStats.cjs +124 -0
  41. package/src/main/lib/promptSessionSchema.cjs +1 -1
  42. package/src/main/seedAgentPersonas.cjs +92 -0
  43. package/src/main/seedDevPlugin.cjs +14 -2
  44. package/src/main/seedSchedulerMcp.cjs +148 -0
  45. package/src/main/seedStatus.cjs +59 -0
  46. package/src/preload/api.d.ts +40 -1
  47. package/src/preload/index.cjs +7 -0
  48. package/src/seed/agents/architect.md +64 -0
  49. package/src/seed/agents/dev-lead.md +42 -0
  50. package/src/seed/prompts/code-review/ac-coverage-check.md +8 -0
  51. package/src/seed/prompts/code-review/correctness-only.md +8 -0
  52. package/src/seed/prompts/code-review/full-spectrum-high.md +8 -0
  53. package/src/seed/prompts/code-review/hallucination-check.md +8 -0
  54. package/src/seed/prompts/code-review/public-api-compat.md +8 -0
  55. package/src/seed/prompts/code-review/readability-naming.md +8 -0
  56. package/src/seed/prompts/debugging/bug-as-failing-test.md +8 -0
  57. package/src/seed/prompts/debugging/git-bisect-regression.md +8 -0
  58. package/src/seed/prompts/debugging/instrument-intermittent-bug.md +8 -0
  59. package/src/seed/prompts/debugging/localize-pipeline-failure.md +8 -0
  60. package/src/seed/prompts/debugging/reproduce-then-diagnose.md +8 -0
  61. package/src/seed/prompts/documentation/adr-from-change.md +8 -0
  62. package/src/seed/prompts/documentation/module-readme.md +8 -0
  63. package/src/seed/prompts/documentation/onboarding-plan.md +8 -0
  64. package/src/seed/prompts/documentation/refresh-claude-md.md +8 -0
  65. package/src/seed/prompts/documentation/tsdoc-public-exports.md +8 -0
  66. package/src/seed/prompts/git-pr/conventional-commit.md +8 -0
  67. package/src/seed/prompts/git-pr/draft-pr-title-body.md +8 -0
  68. package/src/seed/prompts/git-pr/pre-commit-safety-sweep.md +8 -0
  69. package/src/seed/prompts/git-pr/release-notes-block.md +8 -0
  70. package/src/seed/prompts/git-pr/split-large-pr.md +8 -0
  71. package/src/seed/prompts/performance/bundle-startup-audit.md +8 -0
  72. package/src/seed/prompts/performance/complexity-audit.md +8 -0
  73. package/src/seed/prompts/performance/cpu-profile-hot-path.md +8 -0
  74. package/src/seed/prompts/performance/db-query-plan-review.md +8 -0
  75. package/src/seed/prompts/performance/memory-leak-hunt.md +8 -0
  76. package/src/seed/prompts/qa/api-contract-tests.md +8 -0
  77. package/src/seed/prompts/qa/e2e-critical-path.md +8 -0
  78. package/src/seed/prompts/qa/failing-test-for-bug.md +8 -0
  79. package/src/seed/prompts/qa/find-missing-test-coverage.md +8 -0
  80. package/src/seed/prompts/qa/stabilize-flaky-test.md +8 -0
  81. package/src/seed/prompts/qa/tdd-red-first.md +8 -0
  82. package/src/seed/prompts/qa/visual-regression-review.md +8 -0
  83. package/src/seed/prompts/qa/wcag-axe-scan.md +8 -0
  84. package/src/seed/prompts/refactoring/dead-code-sweep.md +8 -0
  85. package/src/seed/prompts/refactoring/extract-duplicated-pattern.md +8 -0
  86. package/src/seed/prompts/refactoring/modernize-legacy-file.md +8 -0
  87. package/src/seed/prompts/refactoring/reduce-cyclomatic-complexity.md +8 -0
  88. package/src/seed/prompts/refactoring/tighten-module-boundaries.md +8 -0
  89. package/src/seed/prompts/security/authz-audit.md +8 -0
  90. package/src/seed/prompts/security/crypto-correctness.md +8 -0
  91. package/src/seed/prompts/security/cwe-top-25-hunt.md +8 -0
  92. package/src/seed/prompts/security/dependency-audit.md +8 -0
  93. package/src/seed/prompts/security/ipc-boundary-hardening.md +8 -0
  94. package/src/seed/prompts/security/owasp-top-10-staged-diff.md +8 -0
  95. package/src/seed/prompts/security/secret-credential-scan.md +8 -0
  96. package/dist/assets/AgentLibrary-CSnWV6uv.js +0 -3
  97. package/dist/assets/HostBilko-evd1hD8q.js +0 -1
  98. package/dist/assets/ListDetail-BPJb2et3.js +0 -1
  99. package/dist/assets/MarkdownEditor-CACsHVuf.js +0 -1
  100. package/dist/assets/Panel-n-p8OQLc.js +0 -1
  101. package/dist/assets/Plugins-D1ckVgo3.js +0 -2
  102. package/dist/assets/ProvenanceBadge-ByTjDwAX.js +0 -1
  103. package/dist/assets/SaveBar-_ORUCmZw.js +0 -1
  104. package/dist/assets/ScopeSwitcher-Css6Sg7Y.js +0 -1
  105. package/dist/assets/Settings-DlyJjexf.js +0 -3
  106. package/dist/assets/SystemPrompt-DsgF8xsl.js +0 -1
  107. package/dist/assets/TagLibrary-Q5QsKMes.js +0 -1
  108. package/dist/assets/Toggle-BJHhHQOq.js +0 -1
@@ -0,0 +1,137 @@
1
+ /**
2
+ * epicDelegationStats.test.cjs — unit tests for the derived-at-read-time
3
+ * "did this Epic delegate?" counters (prdsQueued from the Epic's own live
4
+ * PRD dir, inlineEdits from its interactive claude session transcript).
5
+ *
6
+ * Run: timeout 120 npx vitest run src/main/lib/__tests__/epicDelegationStats.test.cjs
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ import { test, expect, afterEach } from 'vitest';
12
+ const fs = require('node:fs');
13
+ const os = require('node:os');
14
+ const path = require('node:path');
15
+ const {
16
+ computeEpicDelegationStats,
17
+ countPrdsQueued,
18
+ countInlineEdits,
19
+ isApplicationSourcePath,
20
+ } = require('../epicDelegationStats.cjs');
21
+ const { encodeCwd } = require('../encodeCwd.cjs');
22
+
23
+ const tmpDirs = [];
24
+ function makeTmpCwd() {
25
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'epic-delegation-'));
26
+ tmpDirs.push(dir);
27
+ return dir;
28
+ }
29
+
30
+ afterEach(() => {
31
+ while (tmpDirs.length) {
32
+ fs.rmSync(tmpDirs.pop(), { recursive: true, force: true });
33
+ }
34
+ });
35
+
36
+ function makeTmpHome() {
37
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'epic-delegation-home-'));
38
+ tmpDirs.push(dir);
39
+ return dir;
40
+ }
41
+
42
+ function writeTranscript(homeDir, cwd, sessionId, lines) {
43
+ const dir = path.join(homeDir, '.claude', 'projects', encodeCwd(cwd));
44
+ fs.mkdirSync(dir, { recursive: true });
45
+ const filePath = path.join(dir, `${sessionId}.jsonl`);
46
+ fs.writeFileSync(filePath, lines.map((l) => JSON.stringify(l)).join('\n') + '\n');
47
+ return filePath;
48
+ }
49
+
50
+ function toolUseLine(name, filePath) {
51
+ return {
52
+ type: 'assistant',
53
+ message: { content: [{ type: 'tool_use', name, id: 't1', input: { file_path: filePath } }] },
54
+ };
55
+ }
56
+
57
+ test('countPrdsQueued counts only .md files in the Epic own live prds dir', () => {
58
+ const cwd = makeTmpCwd();
59
+ const prdsDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'epics', 'epic-1', 'prds');
60
+ fs.mkdirSync(prdsDir, { recursive: true });
61
+ fs.writeFileSync(path.join(prdsDir, '01-foo.md'), '# foo');
62
+ fs.writeFileSync(path.join(prdsDir, '02-bar.md'), '# bar');
63
+ fs.writeFileSync(path.join(prdsDir, '.reserved-03'), '');
64
+ expect(countPrdsQueued(cwd, 'epic-1')).toBe(2);
65
+ });
66
+
67
+ test('countPrdsQueued returns 0 when the prds dir does not exist', () => {
68
+ const cwd = makeTmpCwd();
69
+ expect(countPrdsQueued(cwd, 'missing-epic')).toBe(0);
70
+ });
71
+
72
+ test('isApplicationSourcePath matches src/scripts/plugins/bin, rejects everything else', () => {
73
+ const cwd = '/repo';
74
+ expect(isApplicationSourcePath(cwd, '/repo/src/main/index.cjs')).toBe(true);
75
+ expect(isApplicationSourcePath(cwd, '/repo/scripts/hooks/foo.cjs')).toBe(true);
76
+ expect(isApplicationSourcePath(cwd, '/repo/plugins/dev/skill.md')).toBe(true);
77
+ expect(isApplicationSourcePath(cwd, '/repo/bin/cli.cjs')).toBe(true);
78
+ expect(isApplicationSourcePath(cwd, '/repo/session-manager-operations/scheduler/epics/e1/prds/01.md')).toBe(false);
79
+ expect(isApplicationSourcePath(cwd, '/repo/README.md')).toBe(false);
80
+ expect(isApplicationSourcePath(cwd, '/outside/src/index.cjs')).toBe(false);
81
+ expect(isApplicationSourcePath(cwd, undefined)).toBe(false);
82
+ });
83
+
84
+ test('countInlineEdits counts Write/Edit/NotebookEdit tool_use blocks under application source only', () => {
85
+ const cwd = makeTmpCwd();
86
+ const homeDir = makeTmpHome();
87
+ writeTranscript(homeDir, cwd, 'sess-1', [
88
+ toolUseLine('Write', path.join(cwd, 'src', 'main', 'foo.cjs')),
89
+ toolUseLine('Edit', path.join(cwd, 'src', 'main', 'foo.cjs')),
90
+ toolUseLine('Read', path.join(cwd, 'src', 'main', 'foo.cjs')),
91
+ toolUseLine('Write', path.join(cwd, 'session-manager-operations', 'scheduler', 'epics', 'e1', 'prds', '01.md')),
92
+ toolUseLine('NotebookEdit', path.join(cwd, 'scripts', 'nb.ipynb')),
93
+ ]);
94
+ expect(countInlineEdits(cwd, 'sess-1', { homeDir })).toBe(3);
95
+ });
96
+
97
+ test('countInlineEdits returns 0 when the transcript file does not exist', () => {
98
+ const cwd = makeTmpCwd();
99
+ const homeDir = makeTmpHome();
100
+ expect(countInlineEdits(cwd, 'no-such-session', { homeDir })).toBe(0);
101
+ });
102
+
103
+ test('countInlineEdits returns 0 when claudeSessionId is missing', () => {
104
+ const cwd = makeTmpCwd();
105
+ expect(countInlineEdits(cwd, null)).toBe(0);
106
+ });
107
+
108
+ test('countInlineEdits tolerates a malformed trailing line', () => {
109
+ const cwd = makeTmpCwd();
110
+ const homeDir = makeTmpHome();
111
+ const dir = path.join(homeDir, '.claude', 'projects', encodeCwd(cwd));
112
+ fs.mkdirSync(dir, { recursive: true });
113
+ const filePath = path.join(dir, 'sess-2.jsonl');
114
+ fs.writeFileSync(
115
+ filePath,
116
+ JSON.stringify(toolUseLine('Write', path.join(cwd, 'src', 'a.ts'))) + '\n{not json\n',
117
+ );
118
+ expect(countInlineEdits(cwd, 'sess-2', { homeDir })).toBe(1);
119
+ });
120
+
121
+ test('computeEpicDelegationStats combines both counters and never throws on missing state', () => {
122
+ const cwd = makeTmpCwd();
123
+ const homeDir = makeTmpHome();
124
+ expect(computeEpicDelegationStats(cwd, 'ghost-epic', 'ghost-session', { homeDir })).toEqual({
125
+ prdsQueued: 0,
126
+ inlineEdits: 0,
127
+ });
128
+
129
+ const prdsDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'epics', 'epic-2', 'prds');
130
+ fs.mkdirSync(prdsDir, { recursive: true });
131
+ fs.writeFileSync(path.join(prdsDir, '01-foo.md'), '# foo');
132
+ writeTranscript(homeDir, cwd, 'sess-3', [toolUseLine('Edit', path.join(cwd, 'src', 'x.ts'))]);
133
+ expect(computeEpicDelegationStats(cwd, 'epic-2', 'sess-3', { homeDir })).toEqual({
134
+ prdsQueued: 1,
135
+ inlineEdits: 1,
136
+ });
137
+ });
@@ -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
+ };
@@ -41,7 +41,7 @@ const EpicSourceSchema = z.object({
41
41
  // data so the Epic's first turn can render a structured AIM briefing card
42
42
  // instead of regex-parsing the flat string back apart.
43
43
  const EpicIntakeSectionSchema = z.object({
44
- kind: z.enum(['actor', 'injection', 'input', 'mission', 'goal', 'reference']),
44
+ kind: z.enum(['actor', 'persona-body', 'injection', 'input', 'mission', 'goal', 'reference']),
45
45
  label: z.string(),
46
46
  text: z.string(),
47
47
  source: z.string().optional(),
@@ -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 };
@@ -65,7 +65,7 @@ function writeMarker(state) {
65
65
  }
66
66
  }
67
67
 
68
- async function seedDevPlugin({ logger = console } = {}) {
68
+ async function seedDevPlugin({ logger = console, writeLog = () => {} } = {}) {
69
69
  if (process.env.SM_SEED_DEV_PLUGIN_DISABLE === '1') return;
70
70
  const marker = readMarker();
71
71
  if (marker.done) return; // succeeded before — leave it alone.
@@ -84,12 +84,24 @@ async function seedDevPlugin({ logger = console } = {}) {
84
84
  } else {
85
85
  // Failure: bump the attempt counter so the next boot can retry (bounded).
86
86
  logger.warn?.(`[seedDevPlugin] install failed (exit ${r.exitCode})${r.error ? ` — ${r.error}` : ''}; attempt ${marker.attempts + 1}/${MAX_ATTEMPTS}`);
87
+ writeLog({
88
+ scope: 'seed-dev-plugin',
89
+ level: 'error',
90
+ message: 'install failed',
91
+ meta: { exitCode: r.exitCode, error: r.error, attempt: marker.attempts + 1, maxAttempts: MAX_ATTEMPTS },
92
+ });
87
93
  writeMarker({ done: false, attempts: marker.attempts + 1 });
88
94
  }
89
95
  } catch (err) {
90
96
  logger.warn?.('[seedDevPlugin] error:', err?.message ?? err);
97
+ writeLog({
98
+ scope: 'seed-dev-plugin',
99
+ level: 'error',
100
+ message: 'seed error',
101
+ meta: { error: err?.message ?? String(err), attempt: marker.attempts + 1, maxAttempts: MAX_ATTEMPTS },
102
+ });
91
103
  writeMarker({ done: false, attempts: marker.attempts + 1 });
92
104
  }
93
105
  }
94
106
 
95
- module.exports = { seedDevPlugin };
107
+ module.exports = { seedDevPlugin, markerPath, MAX_ATTEMPTS };