claude-code-session-manager 0.57.1 → 0.59.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 (68) hide show
  1. package/README.md +2 -2
  2. package/dist/assets/{TiptapBody-CJDxVF7X.js → TiptapBody-PUx4oZTh.js} +1 -1
  3. package/dist/assets/index-BPnfPdLW.js +3222 -0
  4. package/dist/assets/{index-WUc_GHMf.css → index-CPMP2XZ_.css} +1 -1
  5. package/dist/index.html +2 -2
  6. package/package.json +1 -2
  7. package/plugins/session-manager-dev/skills/develop/SKILL.md +7 -6
  8. package/plugins/session-manager-dev/skills/develop/standards.md +1 -1
  9. package/plugins/session-manager-dev/skills/explain-to-me/SKILL.md +1 -1
  10. package/plugins/session-manager-dev/skills/find-opportunity/SKILL.md +1 -1
  11. package/plugins/session-manager-dev/skills/ops-sweep/SKILL.md +16 -25
  12. package/plugins/session-manager-dev/skills/project-status/SKILL.md +5 -5
  13. package/scripts/lib/watchdogHelpers.cjs +0 -434
  14. package/scripts/mint-epic.cjs +6 -6
  15. package/src/main/__tests__/activeIndexMerge.test.cjs +2 -2
  16. package/src/main/__tests__/agentModelResolve.test.cjs +119 -0
  17. package/src/main/__tests__/classifyTranscriptLine.test.cjs +128 -17
  18. package/src/main/__tests__/epicContextDigest.test.cjs +100 -0
  19. package/src/main/__tests__/epicMint.test.cjs +165 -207
  20. package/src/main/__tests__/epicValidationHook.test.cjs +291 -0
  21. package/src/main/__tests__/prdMigration.test.cjs +160 -0
  22. package/src/main/__tests__/projectPages.test.cjs +151 -0
  23. package/src/main/__tests__/promptSessionEvents.test.cjs +74 -0
  24. package/src/main/__tests__/promptSessionSchema.test.cjs +101 -0
  25. package/src/main/__tests__/promptSessionsCreateEpicHandler.test.cjs +139 -0
  26. package/src/main/__tests__/rcaReport.test.cjs +188 -0
  27. package/src/main/__tests__/runVerify.test.cjs +109 -6
  28. package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +32 -12
  29. package/src/main/__tests__/scheduler-epic-digest.test.cjs +214 -0
  30. package/src/main/__tests__/scheduler-heal-refusal.test.cjs +61 -0
  31. package/src/main/__tests__/scheduler-notify-originating-tab.test.cjs +74 -0
  32. package/src/main/__tests__/scheduler-writeprd-epic-rollback.test.cjs +5 -5
  33. package/src/main/__tests__/transcripts-doFlush-array.test.cjs +118 -0
  34. package/src/main/__tests__/transcripts-paged-reads.test.cjs +233 -0
  35. package/src/main/__tests__/uniquePrdNumbers.test.cjs +7 -2
  36. package/src/main/chatRunner.cjs +10 -1
  37. package/src/main/health.cjs +5 -1
  38. package/src/main/index.cjs +3 -5
  39. package/src/main/ipcSchemas.cjs +72 -4
  40. package/src/main/lib/__tests__/schedulerBatchDepends.test.cjs +130 -0
  41. package/src/main/lib/agentModelResolve.cjs +97 -0
  42. package/src/main/lib/auditLog.cjs +2 -2
  43. package/src/main/lib/classifyTranscriptLine.cjs +131 -48
  44. package/src/main/lib/epicContextDigest.cjs +78 -0
  45. package/src/main/lib/epicMint.cjs +86 -164
  46. package/src/main/lib/epicValidationHook.cjs +192 -0
  47. package/src/main/lib/prdMigration.cjs +76 -5
  48. package/src/main/lib/promptSessionSchema.cjs +95 -0
  49. package/src/main/lib/promptSessionsCreateEpic.cjs +70 -0
  50. package/src/main/lib/{rcaFeedbackHook.cjs → rcaReport.cjs} +47 -114
  51. package/src/main/lib/schedulerBatch.cjs +70 -111
  52. package/src/main/lib/schedulerConfig.cjs +0 -1
  53. package/src/main/otel.cjs +3 -1
  54. package/src/main/projectPages.cjs +60 -14
  55. package/src/main/promptSessionEvents.cjs +16 -1
  56. package/src/main/runVerify.cjs +17 -2
  57. package/src/main/scheduler.cjs +302 -68
  58. package/src/main/templates/project-pages-default-home.html +123 -0
  59. package/src/main/transcripts.cjs +191 -32
  60. package/src/main/webRemote.cjs +8 -7
  61. package/src/preload/api.d.ts +100 -57
  62. package/src/preload/index.cjs +9 -7
  63. package/dist/assets/index-DCa3QvAi.js +0 -3227
  64. package/plugins/session-manager-dev/skills/propose-epic/SKILL.md +0 -68
  65. package/scripts/propose-epic.cjs +0 -56
  66. package/src/main/__tests__/rcaFeedbackHook.test.cjs +0 -280
  67. package/src/main/repoAnalyzer.cjs +0 -346
  68. package/src/main/search.cjs +0 -332
@@ -8,21 +8,29 @@
8
8
 
9
9
  import { test, expect } from 'vitest';
10
10
 
11
- const { classifyLine, trimContentArray, makeRaw, MAX_RAW_STR, EXEMPT_TYPES } = require('../lib/classifyTranscriptLine.cjs');
11
+ const {
12
+ classifyLine,
13
+ trimContentArray,
14
+ makeRaw,
15
+ buildPreviewText,
16
+ MAX_RAW_STR,
17
+ PREVIEW_CHARS,
18
+ EXEMPT_TYPES,
19
+ } = require('../lib/classifyTranscriptLine.cjs');
12
20
 
13
- test('classifyLine returns null for non-object input', () => {
14
- expect(classifyLine(null)).toBeNull();
15
- expect(classifyLine('not an object')).toBeNull();
21
+ test('classifyLine returns [] for non-object input', () => {
22
+ expect(classifyLine(null)).toEqual([]);
23
+ expect(classifyLine('not an object')).toEqual([]);
16
24
  });
17
25
 
18
26
  test('classifyLine tags usage events', () => {
19
- const ev = classifyLine({ usage: { input_tokens: 10, output_tokens: 5 } });
27
+ const [ev] = classifyLine({ usage: { input_tokens: 10, output_tokens: 5 } });
20
28
  expect(ev.kind).toBe('usage');
21
29
  expect(ev.data).toEqual({ input_tokens: 10, output_tokens: 5 });
22
30
  });
23
31
 
24
32
  test('classifyLine tags TodoWrite tool_use as todo_write', () => {
25
- const ev = classifyLine({
33
+ const [ev] = classifyLine({
26
34
  message: { content: [{ type: 'tool_use', name: 'TodoWrite', input: { todos: [{ content: 'x' }] } }] },
27
35
  });
28
36
  expect(ev.kind).toBe('todo_write');
@@ -30,14 +38,14 @@ test('classifyLine tags TodoWrite tool_use as todo_write', () => {
30
38
  });
31
39
 
32
40
  test('classifyLine tags ExitPlanMode as plan', () => {
33
- const ev = classifyLine({
41
+ const [ev] = classifyLine({
34
42
  message: { content: [{ type: 'tool_use', name: 'ExitPlanMode', input: { plan: 'do things' } }] },
35
43
  });
36
44
  expect(ev.kind).toBe('plan');
37
45
  });
38
46
 
39
47
  test('classifyLine tags Agent/Task tool_use as agent_spawn with toolUseId', () => {
40
- const ev = classifyLine({
48
+ const [ev] = classifyLine({
41
49
  message: { content: [{ type: 'tool_use', name: 'Agent', id: 'tool-1', input: { description: 'do work' } }] },
42
50
  });
43
51
  expect(ev.kind).toBe('agent_spawn');
@@ -45,7 +53,7 @@ test('classifyLine tags Agent/Task tool_use as agent_spawn with toolUseId', () =
45
53
  });
46
54
 
47
55
  test('classifyLine tags generic tool_use blocks', () => {
48
- const ev = classifyLine({
56
+ const [ev] = classifyLine({
49
57
  message: { content: [{ type: 'tool_use', name: 'Bash', id: 'tool-2', input: { command: 'ls' } }] },
50
58
  });
51
59
  expect(ev.kind).toBe('tool_use');
@@ -53,18 +61,62 @@ test('classifyLine tags generic tool_use blocks', () => {
53
61
  });
54
62
 
55
63
  test('classifyLine tags tool_result blocks with toolUseId', () => {
56
- const ev = classifyLine({
64
+ const [ev] = classifyLine({
57
65
  message: { content: [{ type: 'tool_result', tool_use_id: 'tool-1' }] },
58
66
  });
59
67
  expect(ev.kind).toBe('tool_result');
60
68
  expect(ev.data).toEqual({ toolUseId: 'tool-1' });
61
69
  });
62
70
 
63
- test('classifyLine falls back to message kind', () => {
64
- const ev = classifyLine({ type: 'assistant', message: { content: 'hello' } });
71
+ test('classifyLine falls back to message kind for non-array content', () => {
72
+ const [ev] = classifyLine({ type: 'assistant', message: { content: 'hello' } });
65
73
  expect(ev.kind).toBe('assistant');
66
74
  });
67
75
 
76
+ test('CORE: a content array of [text, tool_use, tool_use] emits exactly 3 events, not 1', () => {
77
+ const events = classifyLine({
78
+ type: 'assistant',
79
+ message: {
80
+ content: [
81
+ { type: 'text', text: 'doing two things' },
82
+ { type: 'tool_use', name: 'Bash', id: 'tu-1', input: { command: 'ls' } },
83
+ { type: 'tool_use', name: 'Bash', id: 'tu-2', input: { command: 'pwd' } },
84
+ ],
85
+ },
86
+ });
87
+ expect(events).toHaveLength(3);
88
+ expect(events[0].kind).toBe('assistant'); // text block inherits the message's own type
89
+ expect(events[0].data).toBe('doing two things');
90
+ expect(events[1].kind).toBe('tool_use');
91
+ expect(events[1].data.id).toBe('tu-1');
92
+ expect(events[2].kind).toBe('tool_use');
93
+ expect(events[2].data.id).toBe('tu-2');
94
+ });
95
+
96
+ test('CORE: a message carrying both usage AND non-empty content emits the usage event AND the content event(s)', () => {
97
+ const events = classifyLine({
98
+ type: 'assistant',
99
+ usage: { input_tokens: 100, output_tokens: 40 },
100
+ message: { content: [{ type: 'text', text: 'hello' }] },
101
+ });
102
+ expect(events).toHaveLength(2);
103
+ expect(events.map((e) => e.kind).sort()).toEqual(['assistant', 'usage']);
104
+ const usageEv = events.find((e) => e.kind === 'usage');
105
+ expect(usageEv.data).toEqual({ input_tokens: 100, output_tokens: 40 });
106
+ const textEv = events.find((e) => e.kind === 'assistant');
107
+ expect(textEv.data).toBe('hello');
108
+ });
109
+
110
+ test('unknown/future content block types are surfaced, not dropped', () => {
111
+ const events = classifyLine({
112
+ type: 'assistant',
113
+ message: { content: [{ type: 'server_tool_use', id: 'x', input: {} }] },
114
+ });
115
+ expect(events).toHaveLength(1);
116
+ expect(events[0].kind).toBe('content_server_tool_use');
117
+ expect(events[0].data).toEqual({ type: 'server_tool_use', id: 'x', input: {} });
118
+ });
119
+
68
120
  test('trimContentArray passes through non-array input', () => {
69
121
  expect(trimContentArray('not-an-array')).toBe('not-an-array');
70
122
  });
@@ -76,15 +128,74 @@ test('trimContentArray truncates long text fields past MAX_RAW_STR', () => {
76
128
  expect(block.text.endsWith('…')).toBe(true);
77
129
  });
78
130
 
79
- test('trimContentArray leaves EXEMPT_TYPES blocks untouched', () => {
80
- expect(EXEMPT_TYPES.has('tool_result')).toBe(true);
81
- expect(EXEMPT_TYPES.has('tool_use')).toBe(true);
131
+ test('trimContentArray now trims tool_use/tool_result blocks too (EXEMPT_TYPES is empty — orchestrator.ts/race.ts, the sole prior consumers, were deleted 2026-07-30)', () => {
132
+ expect(EXEMPT_TYPES.size).toBe(0);
82
133
  const longText = 'a'.repeat(MAX_RAW_STR + 100);
83
134
  const [block] = trimContentArray([{ type: 'tool_use', text: longText }]);
84
- expect(block.text.length).toBe(longText.length);
135
+ expect(block.text.length).toBe(MAX_RAW_STR + 1);
136
+ expect(block.text.endsWith('…')).toBe(true);
85
137
  });
86
138
 
87
- test('makeRaw builds slim projection from message content', () => {
139
+ test('makeRaw builds a projection from message content', () => {
88
140
  const raw = makeRaw({ message: { content: [{ type: 'text', text: 'hi' }] } });
89
141
  expect(raw.message.content).toEqual([{ type: 'text', text: 'hi' }]);
90
142
  });
143
+
144
+ test('CORE: makeRaw preserves every top-level field on the line, not just message.content', () => {
145
+ const line = {
146
+ type: 'assistant',
147
+ message: { content: [{ type: 'text', text: 'hi' }] },
148
+ attributionSkill: 'develop',
149
+ attributionPlugin: 'session-manager-dev',
150
+ attributionMcpServer: 'some-server',
151
+ attributionMcpTool: 'some_tool',
152
+ effort: 'high',
153
+ gitBranch: 'main',
154
+ isSidechain: false,
155
+ isMeta: true,
156
+ requestId: 'req-123',
157
+ isApiErrorMessage: false,
158
+ interruptedByShutdown: false,
159
+ permissionMode: 'default',
160
+ promptSource: 'user',
161
+ toolUseResult: { ok: true },
162
+ };
163
+ const raw = makeRaw(line);
164
+ for (const key of [
165
+ 'attributionSkill',
166
+ 'attributionPlugin',
167
+ 'attributionMcpServer',
168
+ 'attributionMcpTool',
169
+ 'effort',
170
+ 'gitBranch',
171
+ 'isSidechain',
172
+ 'isMeta',
173
+ 'requestId',
174
+ 'isApiErrorMessage',
175
+ 'interruptedByShutdown',
176
+ 'permissionMode',
177
+ 'promptSource',
178
+ 'toolUseResult',
179
+ ]) {
180
+ expect(raw[key]).toEqual(line[key]);
181
+ }
182
+ });
183
+
184
+ test('CORE: every event carries a bounded previewText and, when a ref is passed, the ref survives untouched', () => {
185
+ const ref = { filePath: '/tmp/fake.jsonl', byteOffset: 128, byteLength: 64 };
186
+ const [ev] = classifyLine({ type: 'user', message: { content: 'a'.repeat(PREVIEW_CHARS + 500) } }, ref);
187
+ expect(ev.ref).toEqual(ref);
188
+ expect(ev.previewText.length).toBeLessThanOrEqual(PREVIEW_CHARS + 1);
189
+ expect(ev.previewText.endsWith('…')).toBe(true);
190
+ });
191
+
192
+ test('classifyLine omits ref (null) when the caller has no file context', () => {
193
+ const [ev] = classifyLine({ usage: { input_tokens: 1, output_tokens: 1 } });
194
+ expect(ev.ref).toBeNull();
195
+ });
196
+
197
+ test('buildPreviewText caps at PREVIEW_CHARS', () => {
198
+ const s = buildPreviewText('x'.repeat(PREVIEW_CHARS + 50));
199
+ expect(s.length).toBe(PREVIEW_CHARS + 1);
200
+ expect(s.endsWith('…')).toBe(true);
201
+ });
@@ -0,0 +1,100 @@
1
+ /**
2
+ * epicContextDigest.test.cjs — unit tests for buildContextDigest
3
+ * (lib/epicContextDigest.cjs), which composes an Epic's goalText
4
+ * (active-index.json, via epicMint.cjs) with its most-recent transcript
5
+ * turns (promptSessionTranscript.cjs) into a bounded, real digest string.
6
+ *
7
+ * Run: timeout 300 npx vitest run src/main/__tests__/epicContextDigest.test.cjs
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ import { test, expect, afterEach } from 'vitest';
13
+ const fsp = require('node:fs/promises');
14
+ const os = require('node:os');
15
+ const path = require('node:path');
16
+ const { buildContextDigest } = require('../lib/epicContextDigest.cjs');
17
+ const { ensureEpic, MINT_AUTHORITY_NEW_EPIC_UI } = require('../lib/epicMint.cjs');
18
+ const { appendTurn } = require('../promptSessionTranscript.cjs');
19
+ const config = require('../config.cjs');
20
+
21
+ const tmpDirs = [];
22
+ afterEach(async () => {
23
+ while (tmpDirs.length) {
24
+ const d = tmpDirs.pop();
25
+ await fsp.rm(d, { recursive: true, force: true });
26
+ }
27
+ });
28
+
29
+ async function mkCwd() {
30
+ const cwd = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-epiccontextdigest-cwd-'));
31
+ config.addAllowedRoot(cwd);
32
+ tmpDirs.push(cwd);
33
+ return cwd;
34
+ }
35
+
36
+ test('normal digest: goalText followed by recent turns', async () => {
37
+ const cwd = await mkCwd();
38
+ const { epicId } = await ensureEpic(cwd, { goalText: 'Fix the flaky login test', status: 'proposed', mintAuthority: MINT_AUTHORITY_NEW_EPIC_UI });
39
+ await appendTurn(cwd, epicId, { role: 'user', text: 'Please investigate the flaky test.' });
40
+ await appendTurn(cwd, epicId, { role: 'assistant', text: 'Found the race condition, fixing now.' });
41
+
42
+ const digest = await buildContextDigest({ cwd, epicId });
43
+ expect(digest).toContain('Fix the flaky login test');
44
+ expect(digest).toContain('[user] Please investigate the flaky test.');
45
+ expect(digest).toContain('[assistant] Found the race condition, fixing now.');
46
+ });
47
+
48
+ test('missing epicId returns empty string', async () => {
49
+ const cwd = await mkCwd();
50
+ expect(await buildContextDigest({ cwd, epicId: '' })).toBe('');
51
+ expect(await buildContextDigest({ cwd })).toBe('');
52
+ });
53
+
54
+ test('epicId absent from active-index.json returns empty string', async () => {
55
+ const cwd = await mkCwd();
56
+ const digest = await buildContextDigest({ cwd, epicId: 'psess-does-not-exist' });
57
+ expect(digest).toBe('');
58
+ });
59
+
60
+ test('empty transcript still returns goalText only, no throw', async () => {
61
+ const cwd = await mkCwd();
62
+ const { epicId } = await ensureEpic(cwd, { goalText: 'Empty transcript epic', status: 'proposed', mintAuthority: MINT_AUTHORITY_NEW_EPIC_UI });
63
+
64
+ const digest = await buildContextDigest({ cwd, epicId });
65
+ expect(digest).toBe('Empty transcript epic');
66
+ });
67
+
68
+ test('maxChars truncation drops the oldest turns first, never a mid-turn slice', async () => {
69
+ const cwd = await mkCwd();
70
+ const { epicId } = await ensureEpic(cwd, { goalText: 'Truncation epic', status: 'proposed', mintAuthority: MINT_AUTHORITY_NEW_EPIC_UI });
71
+ await appendTurn(cwd, epicId, { role: 'user', text: 'oldest turn text goes here' });
72
+ await appendTurn(cwd, epicId, { role: 'assistant', text: 'middle turn text goes here' });
73
+ await appendTurn(cwd, epicId, { role: 'user', text: 'newest turn text goes here' });
74
+
75
+ // Small enough that only the newest turn (plus goalText) can fit.
76
+ const digest = await buildContextDigest({ cwd, epicId, maxChars: 80 });
77
+
78
+ expect(digest.length).toBeLessThanOrEqual(80);
79
+ expect(digest).toContain('newest turn text goes here');
80
+ expect(digest).not.toContain('oldest turn text goes here');
81
+ expect(digest).not.toContain('middle turn text goes here');
82
+ // No turn line appears partially — every included "[role] " line is whole.
83
+ for (const line of digest.split('\n')) {
84
+ if (line.startsWith('[')) {
85
+ expect(line).toMatch(/^\[(user|assistant)\] .+$/);
86
+ }
87
+ }
88
+ });
89
+
90
+ test('maxChars smaller than goalText alone clamps the header itself, never exceeding the bound', async () => {
91
+ const cwd = await mkCwd();
92
+ const longGoal = 'This is a deliberately long goalText that on its own already exceeds the small maxChars bound used below';
93
+ const { epicId } = await ensureEpic(cwd, { goalText: longGoal, status: 'proposed', mintAuthority: MINT_AUTHORITY_NEW_EPIC_UI });
94
+ await appendTurn(cwd, epicId, { role: 'user', text: 'a turn that cannot possibly fit either' });
95
+
96
+ const digest = await buildContextDigest({ cwd, epicId, maxChars: 20 });
97
+
98
+ expect(digest.length).toBe(20);
99
+ expect(digest).toBe(longGoal.slice(0, 20));
100
+ });