ccsniff 1.1.25 → 1.1.26
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.md +5 -0
- package/package.json +1 -1
- package/src/cli.js +17 -1
- package/src/discipline.js +115 -0
package/README.md
CHANGED
|
@@ -56,8 +56,13 @@ npx ccsniff -f # tail new events live
|
|
|
56
56
|
npx ccsniff --rollup out.ndjson --since 7d
|
|
57
57
|
npx ccsniff --unsloth train.jsonl --since 7d --no-subagents
|
|
58
58
|
npx ccsniff --unsloth train.jsonl --unsloth-format sharegpt --since 7d
|
|
59
|
+
npx ccsniff --git-discipline --since 7d --project myrepo
|
|
60
|
+
npx ccsniff --search-discipline --since 7d
|
|
61
|
+
npx ccsniff --glyph-discipline --since 24h
|
|
59
62
|
```
|
|
60
63
|
|
|
64
|
+
Discipline audits: `--git-discipline` flags `git push` without a prior separate `git status --porcelain` Bash event and raw git push/commit inside gm (spool-dispatching) sessions; `--search-discipline` flags Grep/Glob discovery events inside gm sessions; `--glyph-discipline` flags decorative non-ASCII glyphs in assistant text (code blocks excluded). All compose with `--project`/`--since`.
|
|
65
|
+
|
|
61
66
|
### Unsloth training export
|
|
62
67
|
|
|
63
68
|
`--unsloth <out>` writes one JSONL line per Claude Code session, ready for
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -30,7 +30,7 @@ const FLAGS = {
|
|
|
30
30
|
string: ['since', 'until', 'before', 'after', 'grep', 'igrep', 'cwd', 'project', 'role', 'type', 'tool', 'session', 'sid', 'sess', 'parent', 'rollup', 'format', 'sort', 'unsloth', 'unsloth-format', 'exclude-sess', 'exclude-sid', 'exclude-cwd', 'exclude-project'],
|
|
31
31
|
multi: ['grep', 'igrep', 'role', 'type', 'tool', 'session', 'sid', 'project', 'cwd', 'exclude-sess', 'exclude-sid', 'exclude-cwd', 'exclude-project'],
|
|
32
32
|
number: ['limit', 'head', 'tail-n', 'ctx', 'truncate', 'days'],
|
|
33
|
-
bool: ['json', 'ndjson', 'tail', 'f', 'full', 'reverse', 'invert', 'no-subagents', 'only-subagents', 'no-meta', 'only-meta', 'list-sessions', 'list-projects', 'list-tools', 'text', 'full-history', 'stats', 'count', 'help', 'h'],
|
|
33
|
+
bool: ['json', 'ndjson', 'tail', 'f', 'full', 'reverse', 'invert', 'no-subagents', 'only-subagents', 'no-meta', 'only-meta', 'list-sessions', 'list-projects', 'list-tools', 'text', 'full-history', 'stats', 'count', 'help', 'h', 'git-discipline', 'search-discipline', 'glyph-discipline'],
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
function parseArgs(argv) {
|
|
@@ -109,6 +109,11 @@ OUTPUT
|
|
|
109
109
|
--unsloth <out> write Unsloth training JSONL (one conversation per session per line)
|
|
110
110
|
--unsloth-format <fmt> messages (OpenAI/ChatML, default) | sharegpt
|
|
111
111
|
|
|
112
|
+
AUDIT
|
|
113
|
+
--git-discipline flag git push without prior separate porcelain event; raw git push/commit in gm sessions
|
|
114
|
+
--search-discipline flag Grep/Glob discovery events inside gm (spool-dispatching) sessions
|
|
115
|
+
--glyph-discipline flag decorative non-ASCII glyphs in assistant text (code blocks excluded)
|
|
116
|
+
|
|
112
117
|
LIMITS
|
|
113
118
|
by default, output caps at the 500 most recent matching events (after --limit/--tail-n/--ctx apply)
|
|
114
119
|
--full-history disable the default 500-event cap and dump everything matched
|
|
@@ -244,6 +249,17 @@ if (opts.tail) {
|
|
|
244
249
|
// ---------- one-shot collection (everything else needs the full set)
|
|
245
250
|
const { stats, all } = collect(opts, since);
|
|
246
251
|
|
|
252
|
+
// ---------- discipline audits
|
|
253
|
+
if (opts['git-discipline'] || opts['search-discipline'] || opts['glyph-discipline']) {
|
|
254
|
+
const { gitDiscipline, searchDiscipline, glyphDiscipline } = await import('./discipline.js');
|
|
255
|
+
const rows = all.filter(filter);
|
|
256
|
+
if (opts['git-discipline']) gitDiscipline(rows);
|
|
257
|
+
if (opts['search-discipline']) searchDiscipline(rows);
|
|
258
|
+
if (opts['glyph-discipline']) glyphDiscipline(rows);
|
|
259
|
+
process.stderr.write(`# ${stats.events} events / ${stats.files} files / ${rows.length} in scope\n`);
|
|
260
|
+
process.exit(0);
|
|
261
|
+
}
|
|
262
|
+
|
|
247
263
|
// ---------- list-projects
|
|
248
264
|
if (opts['list-projects']) {
|
|
249
265
|
const projects = new Map();
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
const GM_SPOOL_RE = /\.gm[\\\/]exec-spool[\\\/]in[\\\/]/;
|
|
4
|
+
const GIT_PUSH_RE = /\bgit\s+push\b/;
|
|
5
|
+
const GIT_COMMIT_RE = /\bgit\s+(commit|push)\b/;
|
|
6
|
+
const PORCELAIN_RE = /git\s+status\s+--porcelain/;
|
|
7
|
+
const GLYPH_RE = /[\u{2190}-\u{21FF}\u{2500}-\u{25FF}\u{2600}-\u{27BF}\u{1F000}-\u{1FAFF}]/gu;
|
|
8
|
+
|
|
9
|
+
function groupSessions(rows) {
|
|
10
|
+
const sessions = new Map();
|
|
11
|
+
for (const ev of rows) {
|
|
12
|
+
const sid = ev.conversation?.id || '?';
|
|
13
|
+
if (!sessions.has(sid)) sessions.set(sid, []);
|
|
14
|
+
sessions.get(sid).push(ev);
|
|
15
|
+
}
|
|
16
|
+
for (const evs of sessions.values()) evs.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
|
|
17
|
+
return sessions;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isGmSession(evs) {
|
|
21
|
+
for (const ev of evs) {
|
|
22
|
+
const b = ev.block || {};
|
|
23
|
+
if (b.type !== 'tool_use') continue;
|
|
24
|
+
if (b.name === 'Skill' && b.input?.skill === 'gm') return true;
|
|
25
|
+
if (b.name === 'Write' && GM_SPOOL_RE.test(String(b.input?.file_path || ''))) return true;
|
|
26
|
+
if (b.name === 'Bash' && GM_SPOOL_RE.test(String(b.input?.command || ''))) return true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sample(ev, detail) {
|
|
32
|
+
const sid = (ev.conversation?.id || '?').slice(0, 8);
|
|
33
|
+
const iso = new Date(ev.timestamp || 0).toISOString().slice(0, 19).replace('T', ' ');
|
|
34
|
+
const repo = path.basename(ev.conversation?.cwd || '');
|
|
35
|
+
const text = String(detail).replace(/\s+/g, ' ').slice(0, 160);
|
|
36
|
+
return ` [${iso}] [${repo}] ${sid} ${text}\n`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function report(label, findings, maxSamples) {
|
|
40
|
+
process.stdout.write(`# ${label}: ${findings.length} finding(s)\n`);
|
|
41
|
+
for (const f of findings.slice(0, maxSamples)) process.stdout.write(f);
|
|
42
|
+
if (findings.length > maxSamples) process.stdout.write(` ... ${findings.length - maxSamples} more\n`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function gitDiscipline(rows, maxSamples = 10) {
|
|
46
|
+
const sessions = groupSessions(rows);
|
|
47
|
+
const pushNoPorcelain = [];
|
|
48
|
+
const gmRawGit = [];
|
|
49
|
+
let bashGit = 0;
|
|
50
|
+
for (const evs of sessions.values()) {
|
|
51
|
+
const gm = isGmSession(evs);
|
|
52
|
+
let porcelainSeen = false;
|
|
53
|
+
for (const ev of evs) {
|
|
54
|
+
const b = ev.block || {};
|
|
55
|
+
if (b.type !== 'tool_use' || b.name !== 'Bash') continue;
|
|
56
|
+
const cmd = String(b.input?.command || '');
|
|
57
|
+
if (!/\bgit\b/.test(cmd)) continue;
|
|
58
|
+
bashGit++;
|
|
59
|
+
if (PORCELAIN_RE.test(cmd) && !GIT_PUSH_RE.test(cmd)) porcelainSeen = true;
|
|
60
|
+
if (GIT_PUSH_RE.test(cmd) && !porcelainSeen) pushNoPorcelain.push(sample(ev, cmd));
|
|
61
|
+
if (gm && GIT_COMMIT_RE.test(cmd)) gmRawGit.push(sample(ev, cmd));
|
|
62
|
+
if (GIT_PUSH_RE.test(cmd)) porcelainSeen = false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
process.stdout.write(`# git-discipline: ${sessions.size} sessions, ${bashGit} raw git Bash events\n`);
|
|
66
|
+
report('push without prior separate porcelain event', pushNoPorcelain, maxSamples);
|
|
67
|
+
report('raw git push/commit inside gm session (spool bypass)', gmRawGit, maxSamples);
|
|
68
|
+
return pushNoPorcelain.length + gmRawGit.length;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function searchDiscipline(rows, maxSamples = 10) {
|
|
72
|
+
const sessions = groupSessions(rows);
|
|
73
|
+
const findings = [];
|
|
74
|
+
let gmSessions = 0;
|
|
75
|
+
for (const evs of sessions.values()) {
|
|
76
|
+
if (!isGmSession(evs)) continue;
|
|
77
|
+
gmSessions++;
|
|
78
|
+
for (const ev of evs) {
|
|
79
|
+
const b = ev.block || {};
|
|
80
|
+
if (b.type !== 'tool_use') continue;
|
|
81
|
+
if (b.name !== 'Grep' && b.name !== 'Glob') continue;
|
|
82
|
+
const detail = `${b.name} ${b.input?.pattern || ''}`;
|
|
83
|
+
findings.push(sample(ev, detail));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
process.stdout.write(`# search-discipline: ${sessions.size} sessions, ${gmSessions} gm sessions\n`);
|
|
87
|
+
report('Grep/Glob discovery inside gm session', findings, maxSamples);
|
|
88
|
+
return findings.length;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function stripCode(text) {
|
|
92
|
+
return text.replace(/```[\s\S]*?```/g, '').replace(/`[^`\n]*`/g, '');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function glyphDiscipline(rows, maxSamples = 10) {
|
|
96
|
+
const findings = [];
|
|
97
|
+
let scanned = 0;
|
|
98
|
+
let glyphTotal = 0;
|
|
99
|
+
for (const ev of rows) {
|
|
100
|
+
const b = ev.block || {};
|
|
101
|
+
if (ev.role !== 'assistant' || b.type !== 'text') continue;
|
|
102
|
+
scanned++;
|
|
103
|
+
const text = stripCode(String(b.text || ''));
|
|
104
|
+
const matches = text.match(GLYPH_RE);
|
|
105
|
+
if (!matches || !matches.length) continue;
|
|
106
|
+
glyphTotal += matches.length;
|
|
107
|
+
const uniq = [...new Set(matches)].slice(0, 8).join(' ');
|
|
108
|
+
const ctxIdx = text.search(GLYPH_RE);
|
|
109
|
+
const ctx = text.slice(Math.max(0, ctxIdx - 40), ctxIdx + 40);
|
|
110
|
+
findings.push(sample(ev, `${matches.length}x [${uniq}] ...${ctx}...`));
|
|
111
|
+
}
|
|
112
|
+
process.stdout.write(`# glyph-discipline: ${scanned} assistant text blocks scanned, ${glyphTotal} decorative glyphs\n`);
|
|
113
|
+
report('assistant text with decorative non-ASCII glyphs', findings, maxSamples);
|
|
114
|
+
return findings.length;
|
|
115
|
+
}
|