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/README.ko.md +45 -3
- package/README.md +45 -3
- package/bin/cli.mjs +331 -39
- package/hooks/batch-checkpoint.mjs +9 -22
- package/hooks/build-guard.mjs +7 -12
- package/hooks/build-tracker.mjs +9 -13
- package/hooks/commit-guard.mjs +20 -16
- package/hooks/commit-message.mjs +17 -17
- package/hooks/debug-loop.mjs +11 -22
- package/hooks/file-size-warn.mjs +7 -12
- package/hooks/plan-guard.mjs +14 -24
- package/hooks/scope-guard.mjs +9 -23
- package/hooks/subagent-inject.mjs +6 -12
- package/hooks/tdd-guard.mjs +8 -13
- package/hooks/test-tracker.mjs +10 -13
- package/lib/config.mjs +4 -1
- package/lib/event-log.mjs +96 -0
- package/lib/hook-utils.mjs +69 -0
- package/lib/stats.mjs +32 -5
- package/package.json +1 -1
- package/templates/commands/smith-dashboard.md +10 -0
- package/templates/commands/smith-update.md +1 -1
- package/templates/rules.en.md +3 -3
- package/templates/rules.ja.md +3 -3
- package/templates/rules.ko.md +3 -3
- package/templates/rules.md +3 -3
- package/templates/rules.zh.md +3 -3
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
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
832
|
-
.card { background: #161b22; border: 1px solid #30363d; border-radius:
|
|
833
|
-
.
|
|
834
|
-
.
|
|
835
|
-
.
|
|
836
|
-
.
|
|
837
|
-
.
|
|
838
|
-
.
|
|
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
|
|
854
|
-
<p class="subtitle">
|
|
855
|
-
|
|
856
|
-
<div class="grid">
|
|
857
|
-
<div class="card">
|
|
858
|
-
<div class="
|
|
859
|
-
<div class="
|
|
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="
|
|
863
|
-
<div class="
|
|
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="
|
|
867
|
-
<div class="
|
|
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="
|
|
871
|
-
<div class="
|
|
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
|
-
<
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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:
|
|
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);
|
package/hooks/build-guard.mjs
CHANGED
|
@@ -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
|
-
|
|
18
|
-
|
|
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.
|
|
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
|
}
|