pixelcrew 0.2.3 → 0.2.6

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 (171) hide show
  1. package/README.md +389 -315
  2. package/bin/pixel-agents.js +306 -149
  3. package/package.json +3 -1
  4. package/skill/SKILL.md +185 -0
  5. package/skill/references/anti-ai-rubric.md +30 -0
  6. package/skill/references/anti-ai-slop-encyclopedia.md +181 -0
  7. package/skill/references/commands-reference.md +135 -0
  8. package/skill/references/testing-and-qa-strategy.md +25 -0
  9. package/skill/references/token-efficiency.md +20 -0
  10. package/src/adapters/adapter.interface.js +77 -0
  11. package/src/adapters/antigravity.js +138 -0
  12. package/src/adapters/claude-code.js +149 -0
  13. package/src/adapters/codex.js +131 -0
  14. package/src/adapters/cursor.js +133 -0
  15. package/src/adapters/generic.js +63 -0
  16. package/src/adapters/index.js +15 -0
  17. package/src/adapters/kiro.js +134 -0
  18. package/src/adapters/registry.js +150 -0
  19. package/src/backend/analyzer/complexity-analyzer.js +104 -0
  20. package/src/backend/analyzer/requirement-analyzer.js +213 -0
  21. package/src/backend/analyzer/threat-analyzer.js +122 -0
  22. package/src/backend/api/api-designer.js +90 -0
  23. package/src/backend/api/error-contract.js +132 -0
  24. package/src/backend/api/route-generator.js +92 -0
  25. package/src/backend/api/schema-generator.js +64 -0
  26. package/src/backend/architecture/adr-generator.js +62 -0
  27. package/src/backend/architecture/architecture-reasoner.js +119 -0
  28. package/src/backend/architecture/architecture-selector.js +57 -0
  29. package/src/backend/architecture/tradeoff-analyzer.js +92 -0
  30. package/src/backend/data/data-modeler.js +114 -0
  31. package/src/backend/data/index-analyzer.js +93 -0
  32. package/src/backend/data/query-analyzer.js +41 -0
  33. package/src/backend/data/schema-generator.js +92 -0
  34. package/src/backend/data/transaction-analyzer.js +49 -0
  35. package/src/backend/index.js +67 -0
  36. package/src/backend/observability/logging-engine.js +80 -0
  37. package/src/backend/observability/metrics-engine.js +31 -0
  38. package/src/backend/observability/tracing-engine.js +16 -0
  39. package/src/backend/patterns/pattern-registry.js +74 -0
  40. package/src/backend/patterns/pattern-selector.js +39 -0
  41. package/src/backend/patterns/tradeoff-engine.js +17 -0
  42. package/src/backend/performance/cache-analyzer.js +31 -0
  43. package/src/backend/performance/concurrency-analyzer.js +34 -0
  44. package/src/backend/performance/performance-engine.js +98 -0
  45. package/src/backend/performance/query-optimizer.js +16 -0
  46. package/src/backend/reliability/failure-analyzer.js +22 -0
  47. package/src/backend/reliability/idempotency-engine.js +56 -0
  48. package/src/backend/reliability/resilience-engine.js +141 -0
  49. package/src/backend/reliability/retry-analyzer.js +15 -0
  50. package/src/backend/security/auth-selector.js +33 -0
  51. package/src/backend/security/authorization-engine.js +90 -0
  52. package/src/backend/security/policy-engine.js +49 -0
  53. package/src/backend/security/secret-analyzer.js +39 -0
  54. package/src/backend/security/security-engine.js +122 -0
  55. package/src/backend/security/threat-model.js +59 -0
  56. package/src/backend/synthesis/backend-synthesizer.js +98 -0
  57. package/src/backend/synthesis/infrastructure-synthesizer.js +108 -0
  58. package/src/backend/synthesis/module-synthesizer.js +262 -0
  59. package/src/backend/validation/architecture-validator.js +70 -0
  60. package/src/backend/validation/backend-quality-validator.js +71 -0
  61. package/src/backend/validation/dependency-validator.js +20 -0
  62. package/src/backend/validation/performance-validator.js +38 -0
  63. package/src/backend/validation/security-validator.js +73 -0
  64. package/src/commands/adapt.js +105 -0
  65. package/src/commands/add.js +78 -0
  66. package/src/commands/animate.js +48 -0
  67. package/src/commands/assemble.js +57 -0
  68. package/src/commands/audit.js +93 -0
  69. package/src/commands/backend.js +125 -0
  70. package/src/commands/bento.js +25 -0
  71. package/src/commands/blueprint.js +65 -0
  72. package/src/commands/bolder.js +48 -0
  73. package/src/commands/boss-fight.js +60 -0
  74. package/src/commands/build.js +39 -0
  75. package/src/commands/calibrate.js +25 -0
  76. package/src/commands/chromatic.js +25 -0
  77. package/src/commands/clarify.js +52 -0
  78. package/src/commands/colorize.js +57 -0
  79. package/src/commands/command.interface.js +28 -0
  80. package/src/commands/commands.js +84 -0
  81. package/src/commands/craft.js +57 -0
  82. package/src/commands/crew.js +66 -0
  83. package/src/commands/critique.js +25 -0
  84. package/src/commands/de-slop.js +25 -0
  85. package/src/commands/delight.js +46 -0
  86. package/src/commands/deploy.js +42 -0
  87. package/src/commands/distill.js +48 -0
  88. package/src/commands/doctor.js +89 -0
  89. package/src/commands/document.js +83 -0
  90. package/src/commands/eight-bit.js +25 -0
  91. package/src/commands/extract.js +75 -0
  92. package/src/commands/fix.js +46 -0
  93. package/src/commands/harden.js +85 -0
  94. package/src/commands/index.js +67 -0
  95. package/src/commands/install.js +75 -0
  96. package/src/commands/layout.js +48 -0
  97. package/src/commands/live.js +44 -0
  98. package/src/commands/manifest.js +25 -0
  99. package/src/commands/office.js +25 -0
  100. package/src/commands/onboard.js +46 -0
  101. package/src/commands/oneshot.js +60 -0
  102. package/src/commands/optimize.js +46 -0
  103. package/src/commands/overdrive.js +46 -0
  104. package/src/commands/parser.js +143 -0
  105. package/src/commands/pixelcrew.js +96 -0
  106. package/src/commands/plan.js +66 -0
  107. package/src/commands/polish.js +45 -0
  108. package/src/commands/quieter.js +48 -0
  109. package/src/commands/recap.js +358 -0
  110. package/src/commands/registry.js +353 -0
  111. package/src/commands/render.js +26 -0
  112. package/src/commands/retrofit.js +25 -0
  113. package/src/commands/review.js +118 -0
  114. package/src/commands/roster.js +25 -0
  115. package/src/commands/sentinel.js +25 -0
  116. package/src/commands/shape.js +66 -0
  117. package/src/commands/skills.js +57 -0
  118. package/src/commands/status.js +48 -0
  119. package/src/commands/stop.js +50 -0
  120. package/src/commands/sync.js +58 -0
  121. package/src/commands/typeset.js +58 -0
  122. package/src/commands/warp.js +25 -0
  123. package/src/core/agent-runtime.js +94 -0
  124. package/src/core/event-bus.js +98 -0
  125. package/src/core/index.js +9 -0
  126. package/src/core/orchestrator.js +234 -0
  127. package/src/core/scheduler.js +124 -0
  128. package/src/core/task-graph.js +185 -0
  129. package/src/dashboard/app.js +1172 -30
  130. package/src/dashboard/index.html +223 -23
  131. package/src/dashboard/styles.css +870 -0
  132. package/src/index.js +16 -0
  133. package/src/orchestrator/artifact-graph.js +104 -0
  134. package/src/orchestrator/ast-normalizer.js +147 -0
  135. package/src/orchestrator/code-generator.js +632 -780
  136. package/src/orchestrator/design-engine.js +203 -0
  137. package/src/orchestrator/engine.js +307 -38
  138. package/src/orchestrator/oneshot.js +367 -380
  139. package/src/orchestrator/ontology.js +394 -0
  140. package/src/orchestrator/planner.js +231 -258
  141. package/src/orchestrator/repair-planner.js +67 -0
  142. package/src/orchestrator/requirement-contract.js +177 -0
  143. package/src/orchestrator/requirement-validator.js +39 -0
  144. package/src/orchestrator/semantic-engine.js +582 -0
  145. package/src/orchestrator/semantic-reviewer.js +179 -0
  146. package/src/orchestrator/task-planner.js +111 -0
  147. package/src/orchestrator/telemetry.js +209 -0
  148. package/src/orchestrator/ui-synthesizer.js +245 -0
  149. package/src/orchestrator/visual-critic.js +106 -0
  150. package/src/policies/architecture/layering.policy.js +14 -0
  151. package/src/policies/performance/indexing.policy.js +13 -0
  152. package/src/policies/reliability/resilience.policy.js +13 -0
  153. package/src/policies/security/auth.policy.js +31 -0
  154. package/src/protocol/agent.js +124 -0
  155. package/src/protocol/event.js +78 -0
  156. package/src/protocol/index.js +8 -0
  157. package/src/protocol/skill.js +54 -0
  158. package/src/protocol/task.js +100 -0
  159. package/src/scaffold/claude-generator.js +60 -0
  160. package/src/scaffold/commands-catalog.js +321 -0
  161. package/src/scaffold/cursor-generator.js +74 -0
  162. package/src/scaffold/ide-rules.js +133 -0
  163. package/src/scaffold/init.js +415 -91
  164. package/src/scaffold/installer.js +571 -0
  165. package/src/scaffold/kiro-generator.js +86 -0
  166. package/src/scaffold/skills-bundle.js +520 -0
  167. package/src/scaffold/templates.js +57 -4
  168. package/src/scaffold/workstations.js +364 -0
  169. package/src/server/server.js +283 -40
  170. package/src/utils/fs-safe.js +168 -0
  171. package/src/utils/markdown-report.js +175 -0
@@ -0,0 +1,358 @@
1
+ /**
2
+ * PIXEL CREW — /recap Command
3
+ *
4
+ * Generates a token-optimized recap of the user's recent git activity.
5
+ * Designed for AI agent consumption: compact output, structured data,
6
+ * no redundant prose. Reads git log, diff stats, and file change status.
7
+ */
8
+
9
+ import { execSync } from 'node:child_process';
10
+ import path from 'node:path';
11
+ import { PixelCommand } from './command.interface.js';
12
+ import { MarkdownReportBuilder } from '../utils/markdown-report.js';
13
+
14
+ export class RecapCommand extends PixelCommand {
15
+ constructor() {
16
+ super({
17
+ name: 'recap',
18
+ aliases: ['summary', 'changelog', 'whatdone'],
19
+ description: 'Generate a token-optimized recap of recent git changes',
20
+ usage: '/recap [count=10]',
21
+ category: 'inspection'
22
+ });
23
+ }
24
+
25
+ /**
26
+ * Safely executes a git command in the target directory.
27
+ * Returns empty string on failure (not a git repo, git not installed, etc.)
28
+ */
29
+ _git(cmd, cwd) {
30
+ try {
31
+ return execSync(`git ${cmd}`, { cwd, encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
32
+ } catch {
33
+ return '';
34
+ }
35
+ }
36
+
37
+ async execute(context, args) {
38
+ const cwd = context.targetDir || process.cwd();
39
+ const count = Math.min(Math.max(parseInt(args[0], 10) || 10, 1), 50);
40
+
41
+ // 1. Verify git repo
42
+ const isGit = this._git('rev-parse --is-inside-work-tree', cwd);
43
+ if (isGit !== 'true') {
44
+ return {
45
+ success: false,
46
+ message: 'Not a git repository. Run /recap from a project with git history.',
47
+ output: [
48
+ '╔══════════════════════════════════════════════════════════════════╗',
49
+ '║ PIXEL CREW — SESSION RECAP ║',
50
+ '╚══════════════════════════════════════════════════════════════════╝',
51
+ '',
52
+ '⚠ Not a git repository. No recap available.'
53
+ ].join('\n')
54
+ };
55
+ }
56
+
57
+ // 2. Get total commit count (cap diff range to available commits)
58
+ const totalCommitCount = parseInt(this._git('rev-list --count HEAD', cwd), 10) || 0;
59
+ const effectiveCount = Math.min(count, totalCommitCount);
60
+
61
+ if (effectiveCount === 0) {
62
+ return {
63
+ success: true,
64
+ message: 'No commits found.',
65
+ data: { commits: [], stats: { filesChanged: 0, insertions: 0, deletions: 0 }, fileChanges: [] },
66
+ output: [
67
+ '╔══════════════════════════════════════════════════════════════════╗',
68
+ '║ PIXEL CREW — SESSION RECAP ║',
69
+ '╚══════════════════════════════════════════════════════════════════╝',
70
+ '',
71
+ 'No commits found in this repository.'
72
+ ].join('\n')
73
+ };
74
+ }
75
+
76
+ // 3. Fetch commits — compact one-line format (token-optimized)
77
+ const logRaw = this._git(
78
+ `log -${effectiveCount} --pretty=format:"%h|%aI|%an|%s"`,
79
+ cwd
80
+ );
81
+ const commits = logRaw
82
+ .split('\n')
83
+ .filter(Boolean)
84
+ .map(line => {
85
+ const [hash, date, author, ...msgParts] = line.split('|');
86
+ return { hash, date, author, message: msgParts.join('|') };
87
+ });
88
+
89
+ // 4. Diff stats (insertions / deletions / files changed)
90
+ // Use --shortstat for minimal token output
91
+ let diffRange;
92
+ if (effectiveCount >= totalCommitCount) {
93
+ // Full repo history — diff from root commit's tree
94
+ const rootHash = this._git('rev-list --max-parents=0 HEAD', cwd).split('\n')[0];
95
+ diffRange = `${rootHash}..HEAD`;
96
+ // If only 1 commit, diff against empty tree
97
+ if (totalCommitCount === 1) {
98
+ diffRange = '--root HEAD';
99
+ }
100
+ } else {
101
+ diffRange = `HEAD~${effectiveCount}..HEAD`;
102
+ }
103
+
104
+ // For single-commit repos, use diff-tree which handles root commits
105
+ let shortStat, nameStatus;
106
+ if (totalCommitCount === 1) {
107
+ shortStat = this._git('diff --stat HEAD', cwd).includes('file')
108
+ ? this._git('log --oneline --shortstat -1 HEAD', cwd)
109
+ : '';
110
+ nameStatus = this._git('diff-tree --no-commit-id -r --name-status HEAD', cwd);
111
+ } else {
112
+ shortStat = this._git(`diff --shortstat ${diffRange}`, cwd);
113
+ nameStatus = this._git(`diff --name-status ${diffRange}`, cwd);
114
+ }
115
+ const stats = this._parseShortStat(shortStat);
116
+ const fileChanges = this._parseNameStatus(nameStatus);
117
+ const timeRange = commits.length > 0
118
+ ? `${this._shortDate(commits[commits.length - 1].date)} → ${this._shortDate(commits[0].date)}`
119
+ : 'N/A';
120
+
121
+ // 5. Synthesize structured summary of work done & key modules
122
+ const summaryData = this._synthesizeSummary(commits, fileChanges, stats);
123
+
124
+ // 6. Token-optimized output — structured summary, clear sections, no redundant prose
125
+ const lines = [
126
+ '╔══════════════════════════════════════════════════════════════════╗',
127
+ '║ PIXEL CREW — SESSION RECAP ║',
128
+ '╚══════════════════════════════════════════════════════════════════╝',
129
+ '',
130
+ `RANGE: ${timeRange} | ${effectiveCount} commit${effectiveCount !== 1 ? 's' : ''} | ${stats.filesChanged} file${stats.filesChanged !== 1 ? 's' : ''} | +${stats.insertions} −${stats.deletions}`
131
+ ];
132
+
133
+ // Executive Summary of Work Done
134
+ if (summaryData.summaryBullets.length > 0) {
135
+ lines.push('');
136
+ lines.push('SUMMARY OF WORK DONE:');
137
+ for (const bullet of summaryData.summaryBullets) {
138
+ lines.push(` ${bullet}`);
139
+ }
140
+ }
141
+
142
+ // Key Modules Touched
143
+ if (summaryData.topModules.length > 0) {
144
+ lines.push('');
145
+ lines.push(`KEY MODULES TOUCHED: ${summaryData.topModules.join(' • ')}`);
146
+ }
147
+
148
+ // Commits — one line each, no fluff
149
+ lines.push('');
150
+ lines.push('COMMITS:');
151
+ for (const c of commits) {
152
+ lines.push(` ${c.hash} ${this._shortDate(c.date)} ${c.message}`);
153
+ }
154
+
155
+ // Files — grouped, compact
156
+ if (fileChanges.length > 0) {
157
+ lines.push('');
158
+ lines.push('FILES:');
159
+ const added = fileChanges.filter(f => f.status === 'A');
160
+ const modified = fileChanges.filter(f => f.status === 'M');
161
+ const deleted = fileChanges.filter(f => f.status === 'D');
162
+ const renamed = fileChanges.filter(f => f.status === 'R');
163
+
164
+ if (added.length) lines.push(` + ${added.map(f => f.file).join(', ')}`);
165
+ if (modified.length) lines.push(` ~ ${modified.map(f => f.file).join(', ')}`);
166
+ if (deleted.length) lines.push(` - ${deleted.map(f => f.file).join(', ')}`);
167
+ if (renamed.length) lines.push(` → ${renamed.map(f => f.file).join(', ')}`);
168
+ }
169
+
170
+ const reportBuilder = new MarkdownReportBuilder({
171
+ title: 'Session Activity & Git Changelog Recap',
172
+ command: '/recap',
173
+ category: 'Git History & Workspace Activity',
174
+ agent: 'Lead Orchestrator (Floor 42)',
175
+ project: context.engine?.getConfig?.()?.project || path.basename(cwd),
176
+ status: 'VERIFIED_CLEAN',
177
+ summary: `Git activity recap covering **${effectiveCount} commits** across \`${timeRange}\`.\n\nTotal changes: **${stats.filesChanged} files modified** with **+${stats.insertions} additions** and **−${stats.deletions} deletions**.\n\n### Summary of Accomplishments:\n${summaryData.summaryBullets.map(b => `- **${b.replace(/^•\s*/, '')}**`).join('\n')}\n\n**Primary Modules Touched:** ${summaryData.topModules.join(', ')}`,
178
+ metrics: [
179
+ { name: 'Commits in Scope', target: 'Recent History', value: `${effectiveCount} Commits`, status: 'OK' },
180
+ { name: 'Files Modified', target: 'Affected Scope', value: `${stats.filesChanged} Files`, status: 'OK' },
181
+ { name: 'Lines Inserted', target: 'Additions', value: `+${stats.insertions}`, status: 'OK' },
182
+ { name: 'Lines Deleted', target: 'Deletions', value: `−${stats.deletions}`, status: 'OK' }
183
+ ],
184
+ sections: [
185
+ {
186
+ title: 'Workstream Accomplishments & Highlights',
187
+ icon: '🚀',
188
+ items: summaryData.summaryBullets
189
+ },
190
+ {
191
+ title: 'Recent Commit Timeline',
192
+ icon: '📜',
193
+ table: {
194
+ headers: ['Commit Hash', 'Timestamp', 'Author', 'Commit Message'],
195
+ rows: commits.map(c => [c.hash, this._shortDate(c.date), c.author || 'User', c.message])
196
+ }
197
+ },
198
+ {
199
+ title: 'Categorized File Modifications',
200
+ icon: '📁',
201
+ table: {
202
+ headers: ['Operation', 'Status', 'File Path'],
203
+ rows: fileChanges.map(f => [
204
+ f.status === 'A' ? 'Added' : (f.status === 'M' ? 'Modified' : (f.status === 'D' ? 'Deleted' : 'Renamed')),
205
+ f.status,
206
+ `\`${f.file}\``
207
+ ])
208
+ }
209
+ }
210
+ ]
211
+ });
212
+
213
+ const reportSaveResult = await reportBuilder.save(cwd, `session-recap-${Date.now()}`);
214
+
215
+ return {
216
+ success: true,
217
+ message: `Recap: ${effectiveCount} commits, ${stats.filesChanged} files changed (+${stats.insertions} −${stats.deletions})`,
218
+ data: {
219
+ commits,
220
+ stats,
221
+ fileChanges,
222
+ timeRange,
223
+ count: effectiveCount,
224
+ summary: summaryData,
225
+ reportPath: reportSaveResult.filePath,
226
+ markdown: reportSaveResult.markdown
227
+ },
228
+ output: lines.join('\n')
229
+ };
230
+ }
231
+
232
+ /**
233
+ * Synthesizes a structured, categorized summary of work done across recent commits.
234
+ */
235
+ _synthesizeSummary(commits, fileChanges, stats) {
236
+ const categories = {
237
+ features: [],
238
+ hardening: [],
239
+ refactors: [],
240
+ docs: [],
241
+ tests: [],
242
+ chores: []
243
+ };
244
+
245
+ for (const c of commits) {
246
+ const msg = c.message.toLowerCase();
247
+ if (msg.startsWith('feat') || msg.includes('add') || msg.includes('implement')) {
248
+ if (msg.includes('anti-ai') || msg.includes('anti-slop') || msg.includes('security') || msg.includes('directive')) {
249
+ categories.hardening.push(c);
250
+ } else {
251
+ categories.features.push(c);
252
+ }
253
+ } else if (msg.startsWith('fix') || msg.includes('bug') || msg.includes('repair')) {
254
+ categories.hardening.push(c);
255
+ } else if (msg.startsWith('refactor') || msg.includes('rename') || msg.includes('standardiz') || msg.includes('clean')) {
256
+ categories.refactors.push(c);
257
+ } else if (msg.startsWith('docs') || msg.includes('readme') || msg.includes('guide')) {
258
+ categories.docs.push(c);
259
+ } else if (msg.startsWith('test') || msg.includes('e2e') || msg.includes('benchmark')) {
260
+ categories.tests.push(c);
261
+ } else {
262
+ categories.chores.push(c);
263
+ }
264
+ }
265
+
266
+ // Identify primary modules/directories touched
267
+ const dirCounts = {};
268
+ for (const f of fileChanges) {
269
+ const parts = f.file.split('/');
270
+ let prefix;
271
+ if (parts[0].startsWith('.')) {
272
+ prefix = parts[0];
273
+ } else if (parts.length > 1) {
274
+ prefix = `${parts[0]}/${parts[1]}`;
275
+ } else {
276
+ prefix = parts[0];
277
+ }
278
+ dirCounts[prefix] = (dirCounts[prefix] || 0) + 1;
279
+ }
280
+
281
+ const topModules = Object.entries(dirCounts)
282
+ .sort((a, b) => b[1] - a[1])
283
+ .slice(0, 5)
284
+ .map(([dir, count]) => `${dir} (${count} file${count !== 1 ? 's' : ''})`);
285
+
286
+ // Build human-readable bullet summaries
287
+ const summaryBullets = [];
288
+ if (categories.features.length > 0) {
289
+ const sample = categories.features.slice(0, 2).map(c => c.message.replace(/^feat(\([^)]+\))?:\s*/i, '')).join('; ');
290
+ summaryBullets.push(`Features & Capabilities (${categories.features.length}): ${sample}`);
291
+ }
292
+ if (categories.hardening.length > 0) {
293
+ const sample = categories.hardening.slice(0, 2).map(c => c.message.replace(/^(feat|fix)(\([^)]+\))?:\s*/i, '')).join('; ');
294
+ summaryBullets.push(`Quality & Anti-Slop (${categories.hardening.length}): ${sample}`);
295
+ }
296
+ if (categories.refactors.length > 0) {
297
+ const sample = categories.refactors.slice(0, 2).map(c => c.message.replace(/^refactor(\([^)]+\))?:\s*/i, '')).join('; ');
298
+ summaryBullets.push(`Architecture & Refactoring (${categories.refactors.length}): ${sample}`);
299
+ }
300
+ if (categories.docs.length > 0) {
301
+ const sample = categories.docs.slice(0, 2).map(c => c.message.replace(/^docs(\([^)]+\))?:\s*/i, '')).join('; ');
302
+ summaryBullets.push(`Documentation & Brand (${categories.docs.length}): ${sample}`);
303
+ }
304
+ if (categories.tests.length > 0) {
305
+ const sample = categories.tests.slice(0, 2).map(c => c.message.replace(/^test(\([^)]+\))?:\s*/i, '')).join('; ');
306
+ summaryBullets.push(`Testing & Verification (${categories.tests.length}): ${sample}`);
307
+ }
308
+ if (categories.chores.length > 0 && summaryBullets.length === 0) {
309
+ const sample = categories.chores.slice(0, 2).map(c => c.message).join('; ');
310
+ summaryBullets.push(`Maintenance (${categories.chores.length}): ${sample}`);
311
+ }
312
+
313
+ return {
314
+ categories,
315
+ topModules,
316
+ summaryBullets
317
+ };
318
+ }
319
+
320
+ /**
321
+ * Parses git diff --shortstat output into structured numbers.
322
+ * Example: " 3 files changed, 45 insertions(+), 12 deletions(-)"
323
+ */
324
+ _parseShortStat(raw) {
325
+ const result = { filesChanged: 0, insertions: 0, deletions: 0 };
326
+ if (!raw) return result;
327
+ const filesMatch = raw.match(/(\d+) file/);
328
+ const insMatch = raw.match(/(\d+) insertion/);
329
+ const delMatch = raw.match(/(\d+) deletion/);
330
+ if (filesMatch) result.filesChanged = parseInt(filesMatch[1], 10);
331
+ if (insMatch) result.insertions = parseInt(insMatch[1], 10);
332
+ if (delMatch) result.deletions = parseInt(delMatch[1], 10);
333
+ return result;
334
+ }
335
+
336
+ /**
337
+ * Parses git diff --name-status into file change entries.
338
+ * Example: "M\tsrc/index.js" → { status: 'M', file: 'src/index.js' }
339
+ */
340
+ _parseNameStatus(raw) {
341
+ if (!raw) return [];
342
+ return raw.split('\n').filter(Boolean).map(line => {
343
+ const [status, ...fileParts] = line.split('\t');
344
+ return { status: status.charAt(0), file: fileParts.join('\t') };
345
+ });
346
+ }
347
+
348
+ /** Formats ISO date to compact "Sep 01 15:14" */
349
+ _shortDate(isoStr) {
350
+ try {
351
+ const d = new Date(isoStr);
352
+ const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
353
+ return `${months[d.getMonth()]} ${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`;
354
+ } catch {
355
+ return isoStr || '';
356
+ }
357
+ }
358
+ }