claude-smith 3.0.0 β†’ 3.2.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.
package/bin/cli.mjs CHANGED
@@ -10,12 +10,13 @@
10
10
  * claude-smith --help - Show help
11
11
  */
12
12
 
13
- import { readFileSync, writeFileSync, mkdirSync, cpSync, existsSync, readdirSync, unlinkSync } from 'fs';
13
+ import { readFileSync, writeFileSync, mkdirSync, cpSync, existsSync, readdirSync, unlinkSync, appendFileSync } from 'fs';
14
14
  import { join, dirname, resolve } from 'path';
15
15
  import { tmpdir } from 'os';
16
16
  import { fileURLToPath } from 'url';
17
17
  import { createInterface } from 'readline';
18
18
  import { readStats } from '../lib/stats.mjs';
19
+ import { readEvents } from '../lib/event-log.mjs';
19
20
 
20
21
  const SUPPORTED_LANGS = {
21
22
  en: 'English',
@@ -78,7 +79,7 @@ switch (command) {
78
79
 
79
80
  async function init() {
80
81
  const cwd = process.cwd();
81
- console.log(`\nπŸ”¨ Smith v${VERSION} - init\n`);
82
+ console.log(`\nπŸ•΅οΈ Smith v${VERSION} - init\n`);
82
83
 
83
84
  // 0. Determine language
84
85
  const lang = await resolveLanguage();
@@ -133,11 +134,29 @@ async function init() {
133
134
  console.log(`βœ… Settings created β†’ .claude/settings.json`);
134
135
  }
135
136
 
136
- // 6. Inject rules into CLAUDE.md (language-specific)
137
+ // 6. Create .smith events directory
138
+ const smithDir = join(cwd, '.smith', 'events');
139
+ mkdirSync(smithDir, { recursive: true, mode: 0o700 });
140
+ console.log('βœ… Events directory created β†’ .smith/events/');
141
+
142
+ // Add .smith/ to .gitignore if not already there
143
+ const gitignorePath = join(cwd, '.gitignore');
144
+ if (existsSync(gitignorePath)) {
145
+ const content = readFileSync(gitignorePath, 'utf8');
146
+ if (!content.includes('.smith/')) {
147
+ appendFileSync(gitignorePath, '\n# claude-smith event logs\n.smith/\n');
148
+ console.log('βœ… Added .smith/ to .gitignore');
149
+ }
150
+ } else {
151
+ writeFileSync(gitignorePath, '# claude-smith event logs\n.smith/\n');
152
+ console.log('βœ… Created .gitignore with .smith/');
153
+ }
154
+
155
+ // 7. Inject rules into CLAUDE.md (language-specific)
137
156
  const claudeMd = join(cwd, 'CLAUDE.md');
138
157
  injectRules(claudeMd, lang);
139
158
 
140
- // 7. Generate/update config file with user choices
159
+ // 8. Generate/update config file with user choices
141
160
  const configPath = join(cwd, '.claude-smith.json');
142
161
  const configData = existsSync(configPath)
143
162
  ? JSON.parse(readFileSync(configPath, 'utf8'))
@@ -246,7 +265,7 @@ async function resolveLanguage() {
246
265
 
247
266
  function update() {
248
267
  const cwd = process.cwd();
249
- console.log(`\nπŸ”¨ Smith v${VERSION} - update\n`);
268
+ console.log(`\nπŸ•΅οΈ Smith v${VERSION} - update\n`);
250
269
 
251
270
  // 1. Overwrite hooks (always latest)
252
271
  const hooksDir = join(cwd, '.claude', 'hooks');
@@ -300,7 +319,8 @@ function injectRules(claudeMdPath, lang = 'en') {
300
319
  const fallback = join(TEMPLATES_DIR, 'rules.en.md');
301
320
  const rulesPath = existsSync(rulesFile) ? rulesFile : fallback;
302
321
  const rules = readFileSync(rulesPath, 'utf8')
303
- .replace(/claude-smith v\d+\.\d+\.\d+/g, `claude-smith v${VERSION}`);
322
+ .replace(/claude-smith v\d+\.\d+\.\d+/g, `claude-smith v${VERSION}`)
323
+ .replace(/\{\{SMITH_VERSION\}\}/g, `v${VERSION}`);
304
324
 
305
325
  if (existsSync(claudeMdPath)) {
306
326
  let content = readFileSync(claudeMdPath, 'utf8');
@@ -366,7 +386,7 @@ function check() {
366
386
  const ciMode = process.argv.includes('--ci');
367
387
  const results = { version: VERSION, ok: true, hooks: {}, settings: false, rules: false };
368
388
 
369
- if (!ciMode) console.log(`\nπŸ”¨ Smith v${VERSION} - check\n`);
389
+ if (!ciMode) console.log(`\nπŸ•΅οΈ Smith v${VERSION} - check\n`);
370
390
 
371
391
  // Check hooks
372
392
  const hooksDir = join(cwd, '.claude', 'hooks');
@@ -429,7 +449,7 @@ function check() {
429
449
  }
430
450
 
431
451
  function stats() {
432
- console.log(`\nπŸ”¨ Smith v${VERSION} - stats\n`);
452
+ console.log(`\nπŸ•΅οΈ Smith v${VERSION} - stats\n`);
433
453
 
434
454
  // Try to find stats from any active session
435
455
  const baseDir = join(tmpdir(), '.claude-smith');
@@ -481,7 +501,7 @@ function stats() {
481
501
 
482
502
  async function uninstall() {
483
503
  const cwd = process.cwd();
484
- console.log(`\nπŸ”¨ Smith v${VERSION} - uninstall\n`);
504
+ console.log(`\nπŸ•΅οΈ Smith v${VERSION} - uninstall\n`);
485
505
 
486
506
  const SMITH_HOOKS = ['tdd-guard.mjs', 'commit-guard.mjs', 'test-tracker.mjs', 'debug-loop.mjs',
487
507
  'batch-checkpoint.mjs', 'subagent-inject.mjs', 'commit-message.mjs', 'build-guard.mjs',
@@ -566,11 +586,17 @@ async function uninstall() {
566
586
  }
567
587
  }
568
588
 
589
+ // 5. Note about .smith/ directory
590
+ const smithEventsDir = join(cwd, '.smith');
591
+ if (existsSync(smithEventsDir)) {
592
+ console.log('ℹ️ Event logs at .smith/ preserved. Delete manually if needed.');
593
+ }
594
+
569
595
  console.log(`\nπŸ”¨ claude-smith uninstalled. Run "claude-smith init" to reinstall.\n`);
570
596
  }
571
597
 
572
598
  function report() {
573
- console.log(`\nπŸ”¨ Smith v${VERSION} - session report\n`);
599
+ console.log(`\nπŸ•΅οΈ Smith v${VERSION} - session report\n`);
574
600
 
575
601
  const baseDir = join(tmpdir(), '.claude-smith');
576
602
  if (!existsSync(baseDir)) {
@@ -759,7 +785,7 @@ ${!isPreTool ? "const toolResponse = input.tool_response || {};\n" : ""}
759
785
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
760
786
  }
761
787
 
762
- console.log(`\nπŸ”¨ Smith v${VERSION} - create-hook\n`);
788
+ console.log(`\nπŸ•΅οΈ Smith v${VERSION} - create-hook\n`);
763
789
  console.log(`βœ… Hook created β†’ .claude/hooks/${fileName}`);
764
790
  console.log(`βœ… Registered in .claude/settings.json (${event})`);
765
791
  console.log(`\nNext steps:`);
@@ -778,7 +804,7 @@ function escapeHtml(str) {
778
804
  }
779
805
 
780
806
  function dashboard() {
781
- console.log(`\nπŸ”¨ Smith v${VERSION} - dashboard\n`);
807
+ console.log(`\nπŸ•΅οΈ Smith v${VERSION} - dashboard\n`);
782
808
 
783
809
  const baseDir = join(tmpdir(), '.claude-smith');
784
810
  if (!existsSync(baseDir)) {
@@ -802,6 +828,9 @@ function dashboard() {
802
828
  }
803
829
  }
804
830
 
831
+ // Read project-level events for timeline
832
+ const projectEvents = readEvents(process.cwd(), { days: 7 });
833
+
805
834
  // Calculate totals
806
835
  let totalFire = 0, totalWarn = 0, totalBlock = 0;
807
836
  for (const counts of Object.values(allStats)) {
@@ -811,31 +840,239 @@ function dashboard() {
811
840
  }
812
841
  const complianceRate = totalFire > 0 ? ((1 - totalBlock / totalFire) * 100).toFixed(1) : '100.0';
813
842
 
843
+ // Calculate hero card values (value-proof metrics)
844
+ const getStat = (hookName, type) => (allStats[hookName]?.[type] || 0);
845
+ const brokenCommitsPrevented = getStat('commit-guard', 'block') + getStat('commit-guard', 'warn') + getStat('build-guard', 'block');
846
+ const unplannedChangesStopped = getStat('plan-guard', 'warn') + getStat('plan-guard', 'block');
847
+ const testFirstReminders = getStat('tdd-guard', 'warn');
848
+ const subagentsDisciplined = getStat('subagent-inject', 'fire');
849
+
850
+ // Calculate counterfactual bullets
851
+ const commitsWithoutTests = getStat('commit-guard', 'warn') + getStat('commit-guard', 'block');
852
+ const changesWithoutPlans = getStat('plan-guard', 'warn') + getStat('plan-guard', 'block');
853
+ const debugLoops = getStat('debug-loop', 'warn') + getStat('debug-loop', 'block');
854
+ const subagentsIgnoringRules = getStat('subagent-inject', 'fire');
855
+ const largeFilesUnchecked = getStat('file-size-warn', 'warn');
856
+
857
+ // Hook insights mapping
858
+ const HOOK_INSIGHTS = {
859
+ 'tdd-guard': {
860
+ label: 'TDD Guard',
861
+ warnMsg: 'Edited code files without corresponding test files.',
862
+ rec: 'Write a failing test before implementing code. Create {name}.test.{ext} alongside source files.'
863
+ },
864
+ 'commit-guard': {
865
+ label: 'Commit Guard',
866
+ warnMsg: 'Attempted commits without recent test runs.',
867
+ blockMsg: 'Blocked commits because tests were failing.',
868
+ rec: 'Run your test suite before every commit. Keep test cycles under 5 minutes.'
869
+ },
870
+ 'commit-message': {
871
+ label: 'Commit Message',
872
+ warnMsg: 'Commit messages didn\'t follow conventional format.',
873
+ rec: 'Use format: type(scope): description (e.g., feat(auth): add login)'
874
+ },
875
+ 'debug-loop': {
876
+ label: 'Debug Loop',
877
+ warnMsg: 'Same file edited repeatedly β€” possible symptom-fixing.',
878
+ blockMsg: 'Editing blocked after too many attempts on the same file.',
879
+ rec: 'Stop and investigate root cause before the next edit. Add logging, check git diff.'
880
+ },
881
+ 'file-size-warn': {
882
+ label: 'File Size',
883
+ warnMsg: 'Files exceeded 500 lines.',
884
+ rec: 'Extract components, hooks, or services into separate files.'
885
+ },
886
+ 'scope-guard': {
887
+ label: 'Scope Guard',
888
+ warnMsg: 'Changes spanned too many directories.',
889
+ rec: 'Break work into smaller, focused commits touching fewer areas.'
890
+ },
891
+ 'batch-checkpoint': {
892
+ label: 'Batch Checkpoint',
893
+ warnMsg: 'Multiple files edited without progress reports.',
894
+ rec: 'Report progress to the user every 3 files.'
895
+ },
896
+ 'plan-guard': {
897
+ label: 'Plan Guard',
898
+ warnMsg: 'Many code files edited without a decomposition plan.',
899
+ blockMsg: 'Editing blocked β€” too many files changed without a plan.',
900
+ rec: 'Create a plan file (docs/plans/*.md) before large changes.'
901
+ },
902
+ 'subagent-inject': {
903
+ label: 'Rule Injection',
904
+ desc: 'Rules were injected into subagent contexts.',
905
+ isGood: true
906
+ },
907
+ 'test-tracker': {
908
+ label: 'Test Tracker',
909
+ desc: 'Test runs were tracked.',
910
+ warnMsg: 'Test coverage decreased between runs.',
911
+ isGood: true
912
+ },
913
+ 'build-tracker': {
914
+ label: 'Build Tracker',
915
+ desc: 'Build results were tracked.',
916
+ isGood: true
917
+ },
918
+ 'build-guard': {
919
+ label: 'Build Guard',
920
+ blockMsg: 'Blocked commits because the build was failing.',
921
+ rec: 'Fix build errors before committing.'
922
+ }
923
+ };
924
+
814
925
  // Generate HTML
815
926
  const hookNames = Object.keys(allStats).sort();
816
927
  const fireData = hookNames.map(h => allStats[h].fire);
817
928
  const warnData = hookNames.map(h => allStats[h].warn);
818
929
  const blockData = hookNames.map(h => allStats[h].block);
819
930
 
931
+ // Generate insights cards
932
+ const insightsHtml = hookNames.map((hookName) => {
933
+ const counts = allStats[hookName];
934
+ const insight = HOOK_INSIGHTS[hookName];
935
+
936
+ // Skip hooks with no activity
937
+ if (counts.fire === 0 && counts.warn === 0 && counts.block === 0) return '';
938
+
939
+ if (!insight) return ''; // Skip unknown hooks
940
+
941
+ const hasBlocks = counts.block > 0;
942
+ const hasWarns = counts.warn > 0;
943
+ const isGood = insight.isGood && !hasWarns && !hasBlocks;
944
+
945
+ // Determine card class and status
946
+ let cardClass = 'insight-card';
947
+ let status = 'βœ…';
948
+ let message = insight.desc || '';
949
+ let recommendation = '';
950
+
951
+ if (hasBlocks) {
952
+ cardClass += ' block';
953
+ status = 'πŸ”΄';
954
+ message = insight.blockMsg || insight.warnMsg || '';
955
+ recommendation = insight.rec || '';
956
+ } else if (hasWarns) {
957
+ cardClass += ' warn';
958
+ status = '⚠️';
959
+ message = insight.warnMsg || '';
960
+ recommendation = insight.rec || '';
961
+ } else if (isGood) {
962
+ cardClass += ' good';
963
+ status = 'βœ…';
964
+ } else {
965
+ return ''; // No warns/blocks and not marked as good
966
+ }
967
+
968
+ // Build count text
969
+ let countText = '';
970
+ if (hasBlocks) countText = `${counts.block} block${counts.block > 1 ? 's' : ''}`;
971
+ else if (hasWarns) countText = `${counts.warn} warning${counts.warn > 1 ? 's' : ''}`;
972
+ else if (isGood) countText = `${counts.fire} trigger${counts.fire > 1 ? 's' : ''}`;
973
+
974
+ return ` <div class="${cardClass}">
975
+ <div class="insight-header">
976
+ <span class="insight-status">${status}</span>
977
+ <span class="insight-name">${escapeHtml(insight.label)}</span>
978
+ <span class="insight-count">${escapeHtml(countText)}</span>
979
+ </div>
980
+ ${message ? `<p class="insight-desc">${escapeHtml(message)}</p>` : ''}
981
+ ${recommendation ? `<p class="insight-rec">πŸ’‘ ${escapeHtml(recommendation)}</p>` : ''}
982
+ </div>`;
983
+ }).filter(Boolean).join('\n');
984
+
985
+ // Generate health summary
986
+ let healthSummary = '';
987
+ const complianceNum = parseFloat(complianceRate);
988
+ if (complianceNum > 90 && totalBlock === 0) {
989
+ healthSummary = '🟒 Excellent session discipline. Keep it up!';
990
+ } else if (complianceNum > 70) {
991
+ healthSummary = '🟑 Good discipline with room for improvement. Focus on the areas below.';
992
+ } else {
993
+ healthSummary = 'πŸ”΄ Multiple enforcement issues detected. Review the insights below.';
994
+ }
995
+
996
+ // Build counterfactual bullets (only show if count > 0)
997
+ const counterfactualBullets = [
998
+ commitsWithoutTests > 0 ? ` <li>πŸ“› <strong>${commitsWithoutTests}</strong> commits without verified tests</li>` : '',
999
+ changesWithoutPlans > 0 ? ` <li>πŸ“‚ <strong>${changesWithoutPlans}</strong> large changes without decomposition plans</li>` : '',
1000
+ debugLoops > 0 ? ` <li>πŸ”„ <strong>${debugLoops}</strong> debug loops (same file edited repeatedly)</li>` : '',
1001
+ subagentsIgnoringRules > 0 ? ` <li>πŸ€– <strong>${subagentsIgnoringRules}</strong> subagents ignoring project rules</li>` : '',
1002
+ largeFilesUnchecked > 0 ? ` <li>πŸ“ <strong>${largeFilesUnchecked}</strong> files growing past 500 lines unchecked</li>` : ''
1003
+ ].filter(Boolean).join('\n');
1004
+
1005
+ // Timeline generation
1006
+ const EVENT_ICONS = {
1007
+ 'tdd-guard': { icon: 'πŸ§ͺ', color: '#d29922' },
1008
+ 'commit-guard': { icon: 'βœ…', color: '#3fb950' },
1009
+ 'commit-message': { icon: 'πŸ“', color: '#d29922' },
1010
+ 'build-guard': { icon: 'πŸ—οΈ', color: '#f85149' },
1011
+ 'test-tracker': { icon: 'πŸ§ͺ', color: '#58a6ff' },
1012
+ 'build-tracker': { icon: 'πŸ—οΈ', color: '#58a6ff' },
1013
+ 'debug-loop': { icon: 'πŸ”„', color: '#f85149' },
1014
+ 'file-size-warn': { icon: 'πŸ“', color: '#d29922' },
1015
+ 'scope-guard': { icon: 'πŸ“‚', color: '#d29922' },
1016
+ 'batch-checkpoint': { icon: 'πŸ“‹', color: '#58a6ff' },
1017
+ 'plan-guard': { icon: 'πŸ“‹', color: '#d29922' },
1018
+ 'subagent-inject': { icon: 'πŸ€–', color: '#3fb950' }
1019
+ };
1020
+
1021
+ const HOOK_LABELS = {
1022
+ 'tdd-guard': 'TDD Guard',
1023
+ 'commit-guard': 'Commit Guard',
1024
+ 'commit-message': 'Commit Msg',
1025
+ 'build-guard': 'Build Guard',
1026
+ 'test-tracker': 'Test Tracker',
1027
+ 'build-tracker': 'Build Tracker',
1028
+ 'debug-loop': 'Debug Loop',
1029
+ 'file-size-warn': 'File Size',
1030
+ 'scope-guard': 'Scope Guard',
1031
+ 'batch-checkpoint': 'Checkpoint',
1032
+ 'plan-guard': 'Plan Guard',
1033
+ 'subagent-inject': 'Agent Inject'
1034
+ };
1035
+
1036
+ const recentEvents = projectEvents.slice(-50).reverse();
1037
+
1038
+ const timelineHtml = recentEvents.length > 0
1039
+ ? recentEvents.map(ev => {
1040
+ const time = new Date(ev.t).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
1041
+ const iconInfo = EVENT_ICONS[ev.h] || { icon: 'πŸ“Œ', color: '#8b949e' };
1042
+ const label = HOOK_LABELS[ev.h] || ev.h;
1043
+ const evClass = ev.e === 'block' ? 'block' : ev.e === 'warn' ? 'warn' : '';
1044
+ const fileDisplay = ev.f ? ev.f.split('/').slice(-2).join('/') : '';
1045
+ return ` <div class="timeline-event ${evClass}">
1046
+ <span class="timeline-time">${escapeHtml(time)}</span>
1047
+ <span class="timeline-icon">${iconInfo.icon}</span>
1048
+ <span class="timeline-hook">${escapeHtml(label)}</span>
1049
+ <span class="timeline-msg">${escapeHtml(ev.m || '')}</span>
1050
+ <span class="timeline-file">${escapeHtml(fileDisplay)}</span>
1051
+ </div>`;
1052
+ }).join('\n')
1053
+ : '';
1054
+
820
1055
  const html = `<!DOCTYPE html>
821
1056
  <html lang="en">
822
1057
  <head>
823
1058
  <meta charset="UTF-8">
824
1059
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
825
- <title>claude-smith Compliance Dashboard</title>
1060
+ <title>πŸ•΅οΈ Smith Dashboard</title>
826
1061
  <style>
827
1062
  * { margin: 0; padding: 0; box-sizing: border-box; }
828
1063
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0d1117; color: #c9d1d9; padding: 2rem; }
829
1064
  h1 { color: #58a6ff; margin-bottom: 0.5rem; }
830
1065
  .subtitle { color: #8b949e; margin-bottom: 2rem; }
831
- .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
832
- .card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 1.5rem; }
833
- .card-label { color: #8b949e; font-size: 0.85rem; text-transform: uppercase; }
834
- .card-value { font-size: 2rem; font-weight: bold; margin-top: 0.5rem; }
835
- .card-value.green { color: #3fb950; }
836
- .card-value.yellow { color: #d29922; }
837
- .card-value.red { color: #f85149; }
838
- .card-value.blue { color: #58a6ff; }
1066
+ .hero-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1.5rem; margin-bottom: 2.5rem; }
1067
+ .hero-card { background: #161b22; border: 1px solid #30363d; border-radius: 12px; padding: 2rem 1.5rem; text-align: center; }
1068
+ .hero-icon { font-size: 2.5rem; margin-bottom: 0.5rem; }
1069
+ .hero-value { font-size: 3rem; font-weight: 800; color: #58a6ff; line-height: 1; }
1070
+ .hero-label { color: #8b949e; font-size: 0.9rem; margin-top: 0.5rem; line-height: 1.3; }
1071
+ .counterfactual { background: linear-gradient(135deg, #1a1e2e 0%, #161b22 100%); border: 1px solid #30363d; border-left: 4px solid #f85149; border-radius: 8px; padding: 1.5rem 2rem; margin-bottom: 2.5rem; }
1072
+ .counterfactual h2 { color: #f0f6fc; font-size: 1.1rem; margin-bottom: 1rem; }
1073
+ .counterfactual ul { list-style: none; }
1074
+ .counterfactual li { padding: 0.4rem 0; color: #c9d1d9; font-size: 0.95rem; }
1075
+ .counterfactual strong { color: #f85149; font-size: 1.1rem; }
839
1076
  table { width: 100%; border-collapse: collapse; background: #161b22; border-radius: 8px; overflow: hidden; }
840
1077
  th, td { padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid #30363d; }
841
1078
  th { background: #21262d; color: #8b949e; font-weight: 600; text-transform: uppercase; font-size: 0.8rem; }
@@ -846,32 +1083,81 @@ function dashboard() {
846
1083
  .legend { display: flex; gap: 1.5rem; margin: 1rem 0; }
847
1084
  .legend-item { display: flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; }
848
1085
  .legend-dot { width: 12px; height: 12px; border-radius: 3px; }
1086
+ .section-title { color: #58a6ff; margin: 2rem 0 0.5rem; font-size: 1.3rem; }
1087
+ .health-summary { color: #8b949e; margin-bottom: 1.5rem; font-size: 1rem; }
1088
+ .insights-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
1089
+ .insight-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 1.25rem; }
1090
+ .insight-card.good { border-left: 3px solid #3fb950; }
1091
+ .insight-card.warn { border-left: 3px solid #d29922; }
1092
+ .insight-card.block { border-left: 3px solid #f85149; }
1093
+ .insight-header { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem; }
1094
+ .insight-name { font-weight: 600; color: #c9d1d9; }
1095
+ .insight-count { color: #8b949e; font-size: 0.85rem; margin-left: auto; }
1096
+ .insight-desc { color: #8b949e; font-size: 0.9rem; margin-bottom: 0.5rem; }
1097
+ .insight-rec { color: #58a6ff; font-size: 0.85rem; }
1098
+ .timeline { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 1rem; max-height: 500px; overflow-y: auto; margin-bottom: 2rem; }
1099
+ .timeline-subtitle { color: #484f58; font-size: 0.85rem; margin-bottom: 1rem; }
1100
+ .timeline-event { display: grid; grid-template-columns: 55px 30px 120px 1fr auto; align-items: center; gap: 0.5rem; padding: 0.5rem 0.75rem; border-bottom: 1px solid #21262d; font-size: 0.85rem; }
1101
+ .timeline-event:last-child { border-bottom: none; }
1102
+ .timeline-event.warn { background: rgba(210, 153, 34, 0.08); }
1103
+ .timeline-event.block { background: rgba(248, 81, 73, 0.1); }
1104
+ .timeline-time { color: #484f58; font-family: monospace; font-size: 0.8rem; }
1105
+ .timeline-icon { font-size: 1rem; text-align: center; }
1106
+ .timeline-hook { color: #8b949e; font-weight: 500; }
1107
+ .timeline-msg { color: #c9d1d9; }
1108
+ .timeline-file { color: #58a6ff; font-family: monospace; font-size: 0.8rem; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1109
+ .timeline-empty { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 2rem; text-align: center; color: #484f58; margin-bottom: 2rem; }
849
1110
  footer { margin-top: 2rem; color: #484f58; font-size: 0.8rem; }
1111
+ @media (max-width: 900px) { .hero-grid { grid-template-columns: repeat(2, 1fr); } }
1112
+ @media (max-width: 700px) { .timeline-event { grid-template-columns: 50px 25px 1fr; } .timeline-file, .timeline-hook { display: none; } }
1113
+ @media (max-width: 500px) { .hero-grid { grid-template-columns: 1fr; } }
850
1114
  </style>
851
1115
  </head>
852
1116
  <body>
853
- <h1>πŸ”¨ claude-smith Dashboard</h1>
854
- <p class="subtitle">Compliance Report β€” generated ${new Date().toISOString().split('T')[0]}</p>
855
-
856
- <div class="grid">
857
- <div class="card">
858
- <div class="card-label">Compliance Rate</div>
859
- <div class="card-value ${parseFloat(complianceRate) >= 90 ? 'green' : parseFloat(complianceRate) >= 70 ? 'yellow' : 'red'}">${escapeHtml(complianceRate)}%</div>
1117
+ <h1>πŸ•΅οΈ Smith</h1>
1118
+ <p class="subtitle">Session Protection Report β€” ${new Date().toISOString().split('T')[0]}</p>
1119
+
1120
+ <div class="hero-grid">
1121
+ <div class="hero-card">
1122
+ <div class="hero-icon">🚫</div>
1123
+ <div class="hero-value">${brokenCommitsPrevented}</div>
1124
+ <div class="hero-label">Broken Commits<br>Prevented</div>
860
1125
  </div>
861
- <div class="card">
862
- <div class="card-label">Total Triggers</div>
863
- <div class="card-value blue">${escapeHtml(totalFire)}</div>
1126
+ <div class="hero-card">
1127
+ <div class="hero-icon">πŸ“‹</div>
1128
+ <div class="hero-value">${unplannedChangesStopped}</div>
1129
+ <div class="hero-label">Unplanned Changes<br>Stopped</div>
864
1130
  </div>
865
- <div class="card">
866
- <div class="card-label">Warnings</div>
867
- <div class="card-value yellow">${escapeHtml(totalWarn)}</div>
1131
+ <div class="hero-card">
1132
+ <div class="hero-icon">πŸ§ͺ</div>
1133
+ <div class="hero-value">${testFirstReminders}</div>
1134
+ <div class="hero-label">Test-First<br>Reminders</div>
868
1135
  </div>
869
- <div class="card">
870
- <div class="card-label">Blocks</div>
871
- <div class="card-value red">${escapeHtml(totalBlock)}</div>
1136
+ <div class="hero-card">
1137
+ <div class="hero-icon">πŸ€–</div>
1138
+ <div class="hero-value">${subagentsDisciplined}</div>
1139
+ <div class="hero-label">Subagents<br>Disciplined</div>
872
1140
  </div>
873
1141
  </div>
874
1142
 
1143
+ ${counterfactualBullets ? `<div class="counterfactual">
1144
+ <h2>⚑ Without Smith, this session would have had:</h2>
1145
+ <ul>
1146
+ ${counterfactualBullets}
1147
+ </ul>
1148
+ </div>` : ''}
1149
+
1150
+ ${recentEvents.length > 0 ? `<h2 class="section-title">πŸ“… Session Timeline</h2>
1151
+ <p class="timeline-subtitle">Last ${recentEvents.length} events</p>
1152
+ <div class="timeline">
1153
+ ${timelineHtml}
1154
+ </div>` : `<h2 class="section-title">πŸ“… Session Timeline</h2>
1155
+ <div class="timeline-empty">
1156
+ <p>No events recorded yet. Events will appear as Smith hooks activate during your sessions.</p>
1157
+ </div>`}
1158
+
1159
+ <h2 class="section-title">πŸ“Š Detailed Activity</h2>
1160
+
875
1161
  <div class="legend">
876
1162
  <div class="legend-item"><div class="legend-dot bar-fire"></div> Fired</div>
877
1163
  <div class="legend-item"><div class="legend-dot bar-warn"></div> Warned</div>
@@ -897,7 +1183,13 @@ ${hookNames.map((name, i) => {
897
1183
  </tbody>
898
1184
  </table>
899
1185
 
900
- <footer>Generated by claude-smith v${escapeHtml(VERSION)} | ${escapeHtml(new Date().toISOString())}</footer>
1186
+ <h2 class="section-title">πŸ’‘ Insights & Recommendations</h2>
1187
+ <p class="health-summary">${escapeHtml(healthSummary)}</p>
1188
+ <div class="insights-grid">
1189
+ ${insightsHtml}
1190
+ </div>
1191
+
1192
+ <footer>Generated by πŸ•΅οΈ Smith v${escapeHtml(VERSION)} | ${escapeHtml(new Date().toISOString())}</footer>
901
1193
  </body>
902
1194
  </html>`;
903
1195
 
@@ -909,7 +1201,7 @@ ${hookNames.map((name, i) => {
909
1201
 
910
1202
  function help() {
911
1203
  console.log(`
912
- πŸ”¨ Smith v${VERSION}
1204
+ πŸ•΅οΈ Smith v${VERSION}
913
1205
  Claude Code workflow enforcement CLI - forging coding discipline into every session
914
1206
 
915
1207
  Usage:
@@ -5,33 +5,23 @@
5
5
  * Counts distinct files edited, reminds to report progress at threshold (default: 3)
6
6
  */
7
7
 
8
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
8
+ import { writeFileSync, existsSync, mkdirSync, readFileSync } from 'fs';
9
9
  import { join } from 'path';
10
10
  import { tmpdir } from 'os';
11
11
  import { getHookConfig } from '../lib/config.mjs';
12
12
  import { recordEvent } from '../lib/stats.mjs';
13
+ import { appendEvent } from '../lib/event-log.mjs';
14
+ import { sanitizeId, parseHookInput, isOmcAutoMode, notifyUser } from '../lib/hook-utils.mjs';
13
15
 
14
16
  const config = getHookConfig('batch-checkpoint', process.cwd());
15
17
  if (!config.enabled) process.exit(0);
16
18
 
17
19
  // Skip in OMC autonomous execution modes (autopilot, ultrawork, ralph)
18
20
  // These modes manage their own progress reporting and checkpoint would interfere
19
- const omcStateDir = join(process.cwd(), '.omc', 'state');
20
- const omcAutoModes = ['autopilot-state.json', 'ultrawork-state.json', 'ralph-state.json'];
21
- const isOmcAutoMode = omcAutoModes.some(f => {
22
- try {
23
- const state = JSON.parse(readFileSync(join(omcStateDir, f), 'utf8'));
24
- return state.active === true || state.status === 'running';
25
- } catch { return false; }
26
- });
27
- if (isOmcAutoMode) process.exit(0);
21
+ if (isOmcAutoMode(process.cwd())) process.exit(0);
28
22
 
29
- let input;
30
- try {
31
- input = JSON.parse(readFileSync(0, 'utf8'));
32
- } catch {
33
- process.exit(0);
34
- }
23
+ const input = parseHookInput();
24
+ if (!input) process.exit(0);
35
25
 
36
26
  const toolName = input.tool_name;
37
27
  const filePath = input.tool_input?.file_path || '';
@@ -42,11 +32,6 @@ if (!filePath) process.exit(0);
42
32
  // Skip non-source files
43
33
  if (!/\.(ts|tsx|js|jsx|py|go|rs|css|json)$/.test(filePath)) process.exit(0);
44
34
 
45
- function sanitizeId(id) {
46
- if (!id || typeof id !== 'string') return 'default';
47
- return id.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128) || 'default';
48
- }
49
-
50
35
  const sessionId = sanitizeId(input.session_id);
51
36
  const stateDir = join(tmpdir(), '.claude-smith', sessionId);
52
37
  mkdirSync(stateDir, { recursive: true, mode: 0o700 });
@@ -71,10 +56,12 @@ if (!editedFiles.includes(filePath)) {
71
56
  // Every N distinct files, trigger checkpoint reminder
72
57
  if (editedFiles.length > 0 && editedFiles.length % config.fileThreshold === 0) {
73
58
  recordEvent(sessionId, 'batch-checkpoint', 'warn');
59
+ appendEvent(process.cwd(), { hook: 'batch-checkpoint', event: 'warn', message: `${editedFiles.length} files edited` });
60
+ notifyUser(config.notify, 'Batch Checkpoint', 'warn', `${editedFiles.length}개 파일 νŽΈμ§‘`);
74
61
  const output = JSON.stringify({
75
62
  hookSpecificOutput: {
76
63
  hookEventName: "PostToolUse",
77
- additionalContext: `πŸ”¨ Smith πŸ“‹ Batch Checkpoint (rule 2-9): ${editedFiles.length} files edited. Report progress to user before continuing.`
64
+ additionalContext: `πŸ•΅οΈ Smith πŸ“‹ Batch Checkpoint (rule 2-9): ${editedFiles.length} files edited. Report progress to user before continuing.`
78
65
  }
79
66
  });
80
67
  process.stdout.write(output);
@@ -10,16 +10,14 @@ import { join } from 'path';
10
10
  import { tmpdir } from 'os';
11
11
  import { getHookConfig } from '../lib/config.mjs';
12
12
  import { recordEvent } from '../lib/stats.mjs';
13
+ import { appendEvent } from '../lib/event-log.mjs';
14
+ import { sanitizeId, parseHookInput, notifyUser } from '../lib/hook-utils.mjs';
13
15
 
14
16
  const config = getHookConfig('build-guard', process.cwd());
15
17
  if (!config.enabled) process.exit(0);
16
18
 
17
- let input;
18
- try {
19
- input = JSON.parse(readFileSync(0, 'utf8'));
20
- } catch {
21
- process.exit(0);
22
- }
19
+ const input = parseHookInput();
20
+ if (!input) process.exit(0);
23
21
 
24
22
  const command = input.tool_input?.command || '';
25
23
 
@@ -27,11 +25,6 @@ if (input.tool_name !== 'Bash') process.exit(0);
27
25
  if (!command.includes('git commit')) process.exit(0);
28
26
  if (command.includes('--allow-empty')) process.exit(0);
29
27
 
30
- function sanitizeId(id) {
31
- if (!id || typeof id !== 'string') return 'default';
32
- return id.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128) || 'default';
33
- }
34
-
35
28
  const sessionId = sanitizeId(input.session_id);
36
29
  const stateDir = join(tmpdir(), '.claude-smith', sessionId);
37
30
  const lastBuildResult = join(stateDir, 'last-build-result');
@@ -42,7 +35,9 @@ if (existsSync(lastBuildResult)) {
42
35
  const result = readFileSync(lastBuildResult, 'utf8').trim();
43
36
  if (result === 'fail') {
44
37
  recordEvent(sessionId, 'build-guard', 'block');
45
- process.stderr.write("πŸ”¨ Smith βœ‹ Commit blocked: last build FAILED. Run build and fix errors before committing.");
38
+ appendEvent(process.cwd(), { hook: 'build-guard', event: 'block', message: 'Build failing' });
39
+ notifyUser(config.notify, 'Build Guard', 'block', 'λΉŒλ“œ μ‹€νŒ¨ β€” 컀밋 차단');
40
+ process.stderr.write("πŸ•΅οΈ Smith βœ‹ Commit blocked: last build FAILED. Run build and fix errors before committing.");
46
41
  process.exit(2);
47
42
  }
48
43
  }