@sentry/warden 0.0.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 (199) hide show
  1. package/.agents/skills/find-bugs/SKILL.md +75 -0
  2. package/.agents/skills/vercel-react-best-practices/AGENTS.md +2934 -0
  3. package/.agents/skills/vercel-react-best-practices/SKILL.md +136 -0
  4. package/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  5. package/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
  6. package/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
  7. package/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
  8. package/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
  9. package/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
  10. package/.agents/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
  11. package/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
  12. package/.agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
  13. package/.agents/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
  14. package/.agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
  15. package/.agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  16. package/.agents/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
  17. package/.agents/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
  18. package/.agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
  19. package/.agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
  20. package/.agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
  21. package/.agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
  22. package/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
  23. package/.agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
  24. package/.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
  25. package/.agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
  26. package/.agents/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
  27. package/.agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
  28. package/.agents/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
  29. package/.agents/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
  30. package/.agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
  31. package/.agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
  32. package/.agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
  33. package/.agents/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
  34. package/.agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  35. package/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
  36. package/.agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
  37. package/.agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  38. package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  39. package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
  40. package/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
  41. package/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
  42. package/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
  43. package/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
  44. package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
  45. package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
  46. package/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
  47. package/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  48. package/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
  49. package/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
  50. package/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
  51. package/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
  52. package/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
  53. package/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
  54. package/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
  55. package/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
  56. package/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
  57. package/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
  58. package/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
  59. package/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
  60. package/.agents/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
  61. package/.claude/settings.json +57 -0
  62. package/.claude/settings.local.json +88 -0
  63. package/.claude/skills/agent-prompt/SKILL.md +54 -0
  64. package/.claude/skills/agent-prompt/references/agentic-patterns.md +94 -0
  65. package/.claude/skills/agent-prompt/references/anti-patterns.md +140 -0
  66. package/.claude/skills/agent-prompt/references/context-design.md +124 -0
  67. package/.claude/skills/agent-prompt/references/core-principles.md +75 -0
  68. package/.claude/skills/agent-prompt/references/model-guidance.md +118 -0
  69. package/.claude/skills/agent-prompt/references/output-formats.md +98 -0
  70. package/.claude/skills/agent-prompt/references/skill-structure.md +115 -0
  71. package/.claude/skills/agent-prompt/references/system-prompts.md +115 -0
  72. package/.claude/skills/notseer/SKILL.md +131 -0
  73. package/.claude/skills/skill-writer/SKILL.md +140 -0
  74. package/.claude/skills/testing-guidelines/SKILL.md +132 -0
  75. package/.claude/skills/warden-skill/SKILL.md +250 -0
  76. package/.claude/skills/warden-skill/references/config-schema.md +133 -0
  77. package/.dex/config.toml +2 -0
  78. package/.github/workflows/ci.yml +33 -0
  79. package/.github/workflows/release.yml +54 -0
  80. package/.github/workflows/warden.yml +40 -0
  81. package/AGENTS.md +89 -0
  82. package/CONTRIBUTING.md +60 -0
  83. package/LICENSE +105 -0
  84. package/README.md +43 -0
  85. package/SPEC.md +263 -0
  86. package/action.yml +87 -0
  87. package/assets/favicon.png +0 -0
  88. package/assets/warden-icon-bw.svg +5 -0
  89. package/assets/warden-icon-purple.png +0 -0
  90. package/assets/warden-icon-purple.svg +5 -0
  91. package/docs/.claude/settings.local.json +11 -0
  92. package/docs/astro.config.mjs +43 -0
  93. package/docs/package.json +19 -0
  94. package/docs/pnpm-lock.yaml +4000 -0
  95. package/docs/public/favicon.svg +5 -0
  96. package/docs/src/components/Code.astro +141 -0
  97. package/docs/src/components/PackageManagerTabs.astro +183 -0
  98. package/docs/src/components/Terminal.astro +212 -0
  99. package/docs/src/layouts/Base.astro +380 -0
  100. package/docs/src/pages/cli.astro +167 -0
  101. package/docs/src/pages/config.astro +394 -0
  102. package/docs/src/pages/guide.astro +449 -0
  103. package/docs/src/pages/index.astro +490 -0
  104. package/docs/src/styles/global.css +551 -0
  105. package/docs/tsconfig.json +3 -0
  106. package/docs/vercel.json +5 -0
  107. package/eslint.config.js +33 -0
  108. package/package.json +73 -0
  109. package/src/action/index.ts +1 -0
  110. package/src/action/main.ts +868 -0
  111. package/src/cli/args.test.ts +477 -0
  112. package/src/cli/args.ts +415 -0
  113. package/src/cli/commands/add.ts +447 -0
  114. package/src/cli/commands/init.test.ts +136 -0
  115. package/src/cli/commands/init.ts +132 -0
  116. package/src/cli/commands/setup-app/browser.ts +38 -0
  117. package/src/cli/commands/setup-app/credentials.ts +45 -0
  118. package/src/cli/commands/setup-app/manifest.ts +48 -0
  119. package/src/cli/commands/setup-app/server.ts +172 -0
  120. package/src/cli/commands/setup-app.ts +156 -0
  121. package/src/cli/commands/sync.ts +114 -0
  122. package/src/cli/context.ts +131 -0
  123. package/src/cli/files.test.ts +155 -0
  124. package/src/cli/files.ts +89 -0
  125. package/src/cli/fix.test.ts +310 -0
  126. package/src/cli/fix.ts +387 -0
  127. package/src/cli/git.test.ts +119 -0
  128. package/src/cli/git.ts +318 -0
  129. package/src/cli/index.ts +14 -0
  130. package/src/cli/main.ts +672 -0
  131. package/src/cli/output/box.ts +235 -0
  132. package/src/cli/output/formatters.test.ts +187 -0
  133. package/src/cli/output/formatters.ts +269 -0
  134. package/src/cli/output/icons.ts +13 -0
  135. package/src/cli/output/index.ts +44 -0
  136. package/src/cli/output/ink-runner.tsx +337 -0
  137. package/src/cli/output/jsonl.test.ts +347 -0
  138. package/src/cli/output/jsonl.ts +126 -0
  139. package/src/cli/output/reporter.ts +435 -0
  140. package/src/cli/output/tasks.ts +374 -0
  141. package/src/cli/output/tty.test.ts +117 -0
  142. package/src/cli/output/tty.ts +60 -0
  143. package/src/cli/output/verbosity.test.ts +40 -0
  144. package/src/cli/output/verbosity.ts +31 -0
  145. package/src/cli/terminal.test.ts +148 -0
  146. package/src/cli/terminal.ts +301 -0
  147. package/src/config/index.ts +3 -0
  148. package/src/config/loader.test.ts +313 -0
  149. package/src/config/loader.ts +103 -0
  150. package/src/config/schema.ts +168 -0
  151. package/src/config/writer.test.ts +119 -0
  152. package/src/config/writer.ts +84 -0
  153. package/src/diff/classify.test.ts +162 -0
  154. package/src/diff/classify.ts +92 -0
  155. package/src/diff/coalesce.test.ts +208 -0
  156. package/src/diff/coalesce.ts +133 -0
  157. package/src/diff/context.test.ts +226 -0
  158. package/src/diff/context.ts +201 -0
  159. package/src/diff/index.ts +4 -0
  160. package/src/diff/parser.test.ts +212 -0
  161. package/src/diff/parser.ts +149 -0
  162. package/src/event/context.ts +132 -0
  163. package/src/event/index.ts +2 -0
  164. package/src/event/schedule-context.ts +101 -0
  165. package/src/examples/examples.integration.test.ts +66 -0
  166. package/src/examples/index.test.ts +101 -0
  167. package/src/examples/index.ts +122 -0
  168. package/src/examples/setup.ts +25 -0
  169. package/src/index.ts +115 -0
  170. package/src/output/dedup.test.ts +419 -0
  171. package/src/output/dedup.ts +607 -0
  172. package/src/output/github-checks.test.ts +300 -0
  173. package/src/output/github-checks.ts +476 -0
  174. package/src/output/github-issues.ts +329 -0
  175. package/src/output/index.ts +5 -0
  176. package/src/output/issue-renderer.ts +197 -0
  177. package/src/output/renderer.test.ts +727 -0
  178. package/src/output/renderer.ts +217 -0
  179. package/src/output/stale.test.ts +375 -0
  180. package/src/output/stale.ts +155 -0
  181. package/src/output/types.ts +34 -0
  182. package/src/sdk/index.ts +1 -0
  183. package/src/sdk/runner.test.ts +806 -0
  184. package/src/sdk/runner.ts +1232 -0
  185. package/src/skills/index.ts +36 -0
  186. package/src/skills/loader.test.ts +300 -0
  187. package/src/skills/loader.ts +423 -0
  188. package/src/skills/remote.test.ts +704 -0
  189. package/src/skills/remote.ts +604 -0
  190. package/src/triggers/matcher.test.ts +277 -0
  191. package/src/triggers/matcher.ts +152 -0
  192. package/src/types/index.ts +194 -0
  193. package/src/utils/async.ts +18 -0
  194. package/src/utils/index.test.ts +84 -0
  195. package/src/utils/index.ts +50 -0
  196. package/tsconfig.json +25 -0
  197. package/vitest.config.ts +8 -0
  198. package/vitest.integration.config.ts +11 -0
  199. package/warden.toml +19 -0
@@ -0,0 +1,347 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { existsSync, readFileSync, rmSync, mkdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { homedir, tmpdir } from 'node:os';
5
+ import {
6
+ writeJsonlReport,
7
+ getRunLogsDir,
8
+ generateRunLogFilename,
9
+ getRunLogPath,
10
+ type JsonlRecord,
11
+ } from './jsonl.js';
12
+ import type { SkillReport } from '../../types/index.js';
13
+
14
+ describe('writeJsonlReport', () => {
15
+ let testDir: string;
16
+
17
+ beforeEach(() => {
18
+ testDir = join(tmpdir(), `warden-test-${Date.now()}`);
19
+ mkdirSync(testDir, { recursive: true });
20
+ });
21
+
22
+ afterEach(() => {
23
+ if (existsSync(testDir)) {
24
+ rmSync(testDir, { recursive: true });
25
+ }
26
+ });
27
+
28
+ it('writes one line per report plus summary', () => {
29
+ const outputPath = join(testDir, 'output.jsonl');
30
+ const reports: SkillReport[] = [
31
+ {
32
+ skill: 'security-review',
33
+ summary: 'Found 1 issue',
34
+ findings: [
35
+ {
36
+ id: 'sec-001',
37
+ severity: 'high',
38
+ title: 'SQL Injection',
39
+ description: 'User input passed directly to query',
40
+ },
41
+ ],
42
+ durationMs: 1234,
43
+ },
44
+ {
45
+ skill: 'code-review',
46
+ summary: 'No issues',
47
+ findings: [],
48
+ durationMs: 567,
49
+ },
50
+ ];
51
+
52
+ writeJsonlReport(outputPath, reports, 2000);
53
+
54
+ expect(existsSync(outputPath)).toBe(true);
55
+
56
+ const content = readFileSync(outputPath, 'utf-8');
57
+ const lines = content.trim().split('\n');
58
+
59
+ // 2 reports + 1 summary = 3 lines
60
+ expect(lines.length).toBe(3);
61
+
62
+ // First line: security-review report
63
+ const record1 = JSON.parse(lines[0]!) as JsonlRecord;
64
+ expect(record1.skill).toBe('security-review');
65
+ expect(record1.findings.length).toBe(1);
66
+ expect(record1.findings[0]!.id).toBe('sec-001');
67
+ expect(record1.durationMs).toBe(1234);
68
+ expect(record1.run.durationMs).toBe(2000);
69
+ expect(record1.run.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/);
70
+
71
+ // Second line: code-review report
72
+ const record2 = JSON.parse(lines[1]!) as JsonlRecord;
73
+ expect(record2.skill).toBe('code-review');
74
+ expect(record2.findings.length).toBe(0);
75
+
76
+ // Third line: summary
77
+ const summary = JSON.parse(lines[2]!);
78
+ expect(summary.type).toBe('summary');
79
+ expect(summary.totalFindings).toBe(1);
80
+ expect(summary.bySeverity.high).toBe(1);
81
+ });
82
+
83
+ it('handles empty reports', () => {
84
+ const outputPath = join(testDir, 'empty.jsonl');
85
+
86
+ writeJsonlReport(outputPath, [], 500);
87
+
88
+ const content = readFileSync(outputPath, 'utf-8');
89
+ const lines = content.trim().split('\n');
90
+
91
+ // Just the summary line
92
+ expect(lines.length).toBe(1);
93
+
94
+ const summary = JSON.parse(lines[0]!);
95
+ expect(summary.type).toBe('summary');
96
+ expect(summary.totalFindings).toBe(0);
97
+ });
98
+
99
+ it('aggregates usage stats in summary', () => {
100
+ const outputPath = join(testDir, 'usage.jsonl');
101
+ const reports: SkillReport[] = [
102
+ {
103
+ skill: 'skill-1',
104
+ summary: 'Done',
105
+ findings: [],
106
+ usage: {
107
+ inputTokens: 100,
108
+ outputTokens: 50,
109
+ cacheReadInputTokens: 10,
110
+ cacheCreationInputTokens: 5,
111
+ costUSD: 0.001,
112
+ },
113
+ },
114
+ {
115
+ skill: 'skill-2',
116
+ summary: 'Done',
117
+ findings: [],
118
+ usage: {
119
+ inputTokens: 200,
120
+ outputTokens: 100,
121
+ cacheReadInputTokens: 20,
122
+ cacheCreationInputTokens: 10,
123
+ costUSD: 0.002,
124
+ },
125
+ },
126
+ ];
127
+
128
+ writeJsonlReport(outputPath, reports, 1000);
129
+
130
+ const content = readFileSync(outputPath, 'utf-8');
131
+ const lines = content.trim().split('\n');
132
+ const summary = JSON.parse(lines[2]!);
133
+
134
+ expect(summary.usage.inputTokens).toBe(300);
135
+ expect(summary.usage.outputTokens).toBe(150);
136
+ expect(summary.usage.cacheReadInputTokens).toBe(30);
137
+ expect(summary.usage.cacheCreationInputTokens).toBe(15);
138
+ expect(summary.usage.costUSD).toBeCloseTo(0.003);
139
+ });
140
+
141
+ it('creates parent directories if they do not exist', () => {
142
+ const outputPath = join(testDir, 'nested', 'deep', 'output.jsonl');
143
+
144
+ writeJsonlReport(outputPath, [], 100);
145
+
146
+ expect(existsSync(outputPath)).toBe(true);
147
+ const content = readFileSync(outputPath, 'utf-8');
148
+ const summary = JSON.parse(content.trim());
149
+ expect(summary.type).toBe('summary');
150
+ });
151
+
152
+ it('counts findings by severity in summary', () => {
153
+ const outputPath = join(testDir, 'severity.jsonl');
154
+ const reports: SkillReport[] = [
155
+ {
156
+ skill: 'review',
157
+ summary: 'Issues found',
158
+ findings: [
159
+ { id: '1', severity: 'critical', title: 'A', description: 'A' },
160
+ { id: '2', severity: 'high', title: 'B', description: 'B' },
161
+ { id: '3', severity: 'high', title: 'C', description: 'C' },
162
+ { id: '4', severity: 'medium', title: 'D', description: 'D' },
163
+ { id: '5', severity: 'low', title: 'E', description: 'E' },
164
+ { id: '6', severity: 'info', title: 'F', description: 'F' },
165
+ ],
166
+ },
167
+ ];
168
+
169
+ writeJsonlReport(outputPath, reports, 1000);
170
+
171
+ const content = readFileSync(outputPath, 'utf-8');
172
+ const lines = content.trim().split('\n');
173
+ const summary = JSON.parse(lines[1]!);
174
+
175
+ expect(summary.totalFindings).toBe(6);
176
+ expect(summary.bySeverity.critical).toBe(1);
177
+ expect(summary.bySeverity.high).toBe(2);
178
+ expect(summary.bySeverity.medium).toBe(1);
179
+ expect(summary.bySeverity.low).toBe(1);
180
+ expect(summary.bySeverity.info).toBe(1);
181
+ });
182
+ });
183
+
184
+ describe('getRunLogsDir', () => {
185
+ const originalEnv = process.env['WARDEN_STATE_DIR'];
186
+
187
+ afterEach(() => {
188
+ if (originalEnv === undefined) {
189
+ delete process.env['WARDEN_STATE_DIR'];
190
+ } else {
191
+ process.env['WARDEN_STATE_DIR'] = originalEnv;
192
+ }
193
+ });
194
+
195
+ it('returns default path when WARDEN_STATE_DIR is not set', () => {
196
+ delete process.env['WARDEN_STATE_DIR'];
197
+ const result = getRunLogsDir();
198
+ expect(result).toBe(join(homedir(), '.local', 'warden', 'runs'));
199
+ });
200
+
201
+ it('uses WARDEN_STATE_DIR when set', () => {
202
+ process.env['WARDEN_STATE_DIR'] = '/custom/state';
203
+ const result = getRunLogsDir();
204
+ expect(result).toBe('/custom/state/runs');
205
+ });
206
+ });
207
+
208
+ describe('generateRunLogFilename', () => {
209
+ it('generates filename with directory name and timestamp', () => {
210
+ const timestamp = new Date('2026-01-29T14:32:15.123Z');
211
+ const result = generateRunLogFilename('/path/to/my-project', timestamp);
212
+ expect(result).toBe('my-project_2026-01-29T14-32-15.123Z.jsonl');
213
+ });
214
+
215
+ it('replaces colons in timestamp with hyphens', () => {
216
+ const timestamp = new Date('2026-01-29T10:05:30.000Z');
217
+ const result = generateRunLogFilename('/some/dir', timestamp);
218
+ expect(result).toMatch(/^\w+_2026-01-29T10-05-30\.000Z\.jsonl$/);
219
+ });
220
+
221
+ it('uses "unknown" for empty directory name', () => {
222
+ const timestamp = new Date('2026-01-29T12:00:00.000Z');
223
+ const result = generateRunLogFilename('/', timestamp);
224
+ expect(result).toBe('unknown_2026-01-29T12-00-00.000Z.jsonl');
225
+ });
226
+
227
+ it('handles directory paths with trailing slash', () => {
228
+ const timestamp = new Date('2026-01-29T12:00:00.000Z');
229
+ // basename handles trailing slashes, so /foo/bar/ becomes 'bar'
230
+ const result = generateRunLogFilename('/foo/bar', timestamp);
231
+ expect(result).toBe('bar_2026-01-29T12-00-00.000Z.jsonl');
232
+ });
233
+ });
234
+
235
+ describe('getRunLogPath', () => {
236
+ const originalEnv = process.env['WARDEN_STATE_DIR'];
237
+
238
+ afterEach(() => {
239
+ if (originalEnv === undefined) {
240
+ delete process.env['WARDEN_STATE_DIR'];
241
+ } else {
242
+ process.env['WARDEN_STATE_DIR'] = originalEnv;
243
+ }
244
+ });
245
+
246
+ it('returns full path combining logs dir and filename', () => {
247
+ delete process.env['WARDEN_STATE_DIR'];
248
+ const timestamp = new Date('2026-01-29T14:32:15.123Z');
249
+ const result = getRunLogPath('/path/to/warden', timestamp);
250
+ expect(result).toBe(
251
+ join(homedir(), '.local', 'warden', 'runs', 'warden_2026-01-29T14-32-15.123Z.jsonl')
252
+ );
253
+ });
254
+
255
+ it('respects WARDEN_STATE_DIR', () => {
256
+ process.env['WARDEN_STATE_DIR'] = '/custom/dir';
257
+ const timestamp = new Date('2026-01-29T14:32:15.123Z');
258
+ const result = getRunLogPath('/my/project', timestamp);
259
+ expect(result).toBe('/custom/dir/runs/project_2026-01-29T14-32-15.123Z.jsonl');
260
+ });
261
+ });
262
+
263
+ describe('automatic run logging integration', () => {
264
+ let testStateDir: string;
265
+ const originalEnv = process.env['WARDEN_STATE_DIR'];
266
+
267
+ beforeEach(() => {
268
+ testStateDir = join(tmpdir(), `warden-state-${Date.now()}`);
269
+ process.env['WARDEN_STATE_DIR'] = testStateDir;
270
+ });
271
+
272
+ afterEach(() => {
273
+ if (existsSync(testStateDir)) {
274
+ rmSync(testStateDir, { recursive: true });
275
+ }
276
+ if (originalEnv === undefined) {
277
+ delete process.env['WARDEN_STATE_DIR'];
278
+ } else {
279
+ process.env['WARDEN_STATE_DIR'] = originalEnv;
280
+ }
281
+ });
282
+
283
+ it('writes run log to auto-generated path', () => {
284
+ const reports: SkillReport[] = [
285
+ {
286
+ skill: 'test-skill',
287
+ summary: 'Test complete',
288
+ findings: [
289
+ { id: 'test-1', severity: 'low', title: 'Test', description: 'Test finding' },
290
+ ],
291
+ durationMs: 100,
292
+ },
293
+ ];
294
+
295
+ const timestamp = new Date('2026-01-29T14:32:15.123Z');
296
+ const runLogPath = getRunLogPath('/path/to/my-project', timestamp);
297
+
298
+ writeJsonlReport(runLogPath, reports, 500);
299
+
300
+ // Verify file was created at expected location
301
+ expect(existsSync(runLogPath)).toBe(true);
302
+ expect(runLogPath).toBe(join(testStateDir, 'runs', 'my-project_2026-01-29T14-32-15.123Z.jsonl'));
303
+
304
+ // Verify content
305
+ const content = readFileSync(runLogPath, 'utf-8');
306
+ const lines = content.trim().split('\n');
307
+ expect(lines.length).toBe(2); // 1 report + 1 summary
308
+
309
+ const record = JSON.parse(lines[0]!) as JsonlRecord;
310
+ expect(record.skill).toBe('test-skill');
311
+ expect(record.findings.length).toBe(1);
312
+ });
313
+
314
+ it('creates nested runs directory automatically', () => {
315
+ const runLogPath = getRunLogPath('/some/project', new Date());
316
+
317
+ // Directory shouldn't exist yet
318
+ expect(existsSync(join(testStateDir, 'runs'))).toBe(false);
319
+
320
+ writeJsonlReport(runLogPath, [], 100);
321
+
322
+ // Now it should exist with the file
323
+ expect(existsSync(runLogPath)).toBe(true);
324
+ });
325
+
326
+ it('handles multiple runs with unique timestamps', () => {
327
+ const timestamp1 = new Date('2026-01-29T14:00:00.000Z');
328
+ const timestamp2 = new Date('2026-01-29T14:01:00.000Z');
329
+
330
+ const path1 = getRunLogPath('/project', timestamp1);
331
+ const path2 = getRunLogPath('/project', timestamp2);
332
+
333
+ expect(path1).not.toBe(path2);
334
+
335
+ writeJsonlReport(path1, [], 100);
336
+ writeJsonlReport(path2, [], 200);
337
+
338
+ expect(existsSync(path1)).toBe(true);
339
+ expect(existsSync(path2)).toBe(true);
340
+
341
+ // Verify they have different durations
342
+ const content1 = JSON.parse(readFileSync(path1, 'utf-8').trim());
343
+ const content2 = JSON.parse(readFileSync(path2, 'utf-8').trim());
344
+ expect(content1.run.durationMs).toBe(100);
345
+ expect(content2.run.durationMs).toBe(200);
346
+ });
347
+ });
@@ -0,0 +1,126 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { basename, dirname, join, resolve } from 'node:path';
4
+ import type { SkillReport, UsageStats } from '../../types/index.js';
5
+ import { countBySeverity } from './formatters.js';
6
+
7
+ /**
8
+ * Get the default run logs directory.
9
+ * Uses WARDEN_STATE_DIR env var if set, otherwise ~/.local/warden/runs
10
+ */
11
+ export function getRunLogsDir(): string {
12
+ const stateDir = process.env['WARDEN_STATE_DIR'];
13
+ if (stateDir) {
14
+ return join(stateDir, 'runs');
15
+ }
16
+ return join(homedir(), '.local', 'warden', 'runs');
17
+ }
18
+
19
+ /**
20
+ * Generate a run log filename from directory name and timestamp.
21
+ * Format: {dirname}_{timestamp}.jsonl
22
+ * Timestamp has colons replaced with hyphens for filesystem compatibility.
23
+ */
24
+ export function generateRunLogFilename(cwd: string, timestamp: Date = new Date()): string {
25
+ const dirName = basename(cwd) || 'unknown';
26
+ const ts = timestamp.toISOString().replace(/:/g, '-');
27
+ return `${dirName}_${ts}.jsonl`;
28
+ }
29
+
30
+ /**
31
+ * Get the full path for an automatic run log.
32
+ */
33
+ export function getRunLogPath(cwd: string, timestamp: Date = new Date()): string {
34
+ return join(getRunLogsDir(), generateRunLogFilename(cwd, timestamp));
35
+ }
36
+
37
+ /**
38
+ * Metadata for a JSONL run record.
39
+ */
40
+ export interface JsonlRunMetadata {
41
+ timestamp: string;
42
+ durationMs: number;
43
+ cwd: string;
44
+ }
45
+
46
+ /**
47
+ * A single JSONL record representing one skill's report.
48
+ */
49
+ export interface JsonlRecord {
50
+ run: JsonlRunMetadata;
51
+ skill: string;
52
+ summary: string;
53
+ findings: SkillReport['findings'];
54
+ metadata?: Record<string, unknown>;
55
+ durationMs?: number;
56
+ usage?: UsageStats;
57
+ }
58
+
59
+ /**
60
+ * Aggregate usage stats from reports.
61
+ */
62
+ function aggregateUsage(reports: SkillReport[]): UsageStats | undefined {
63
+ const usages = reports.map((r) => r.usage).filter((u) => u !== undefined);
64
+ if (usages.length === 0) return undefined;
65
+
66
+ return usages.reduce((acc, u) => ({
67
+ inputTokens: acc.inputTokens + u.inputTokens,
68
+ outputTokens: acc.outputTokens + u.outputTokens,
69
+ cacheReadInputTokens: (acc.cacheReadInputTokens ?? 0) + (u.cacheReadInputTokens ?? 0),
70
+ cacheCreationInputTokens: (acc.cacheCreationInputTokens ?? 0) + (u.cacheCreationInputTokens ?? 0),
71
+ costUSD: acc.costUSD + u.costUSD,
72
+ }));
73
+ }
74
+
75
+ /**
76
+ * Write skill reports to a JSONL file.
77
+ * Each line contains one skill report with run metadata.
78
+ * A final summary line is appended at the end.
79
+ */
80
+ export function writeJsonlReport(
81
+ outputPath: string,
82
+ reports: SkillReport[],
83
+ durationMs: number
84
+ ): void {
85
+ const resolvedPath = resolve(process.cwd(), outputPath);
86
+ const timestamp = new Date().toISOString();
87
+ const cwd = process.cwd();
88
+
89
+ const runMetadata: JsonlRunMetadata = {
90
+ timestamp,
91
+ durationMs,
92
+ cwd,
93
+ };
94
+
95
+ const lines: string[] = [];
96
+
97
+ // Write one line per skill report
98
+ for (const report of reports) {
99
+ const record: JsonlRecord = {
100
+ run: runMetadata,
101
+ skill: report.skill,
102
+ summary: report.summary,
103
+ findings: report.findings,
104
+ metadata: report.metadata,
105
+ durationMs: report.durationMs,
106
+ usage: report.usage,
107
+ };
108
+ lines.push(JSON.stringify(record));
109
+ }
110
+
111
+ // Write a summary line at the end
112
+ const allFindings = reports.flatMap((r) => r.findings);
113
+ const summaryRecord = {
114
+ run: runMetadata,
115
+ type: 'summary',
116
+ totalFindings: allFindings.length,
117
+ bySeverity: countBySeverity(allFindings),
118
+ usage: aggregateUsage(reports),
119
+ };
120
+ lines.push(JSON.stringify(summaryRecord));
121
+
122
+ // Ensure parent directory exists
123
+ mkdirSync(dirname(resolvedPath), { recursive: true });
124
+
125
+ writeFileSync(resolvedPath, lines.join('\n') + '\n');
126
+ }