ccsniff 1.1.5 → 1.1.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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +44 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccsniff",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
4
4
  "description": "Watch Claude Code JSONL output files and emit structured events as a Node.js EventEmitter",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/cli.js CHANGED
@@ -29,7 +29,7 @@ const FLAGS = {
29
29
  string: ['since', 'until', 'before', 'after', 'grep', 'igrep', 'cwd', 'project', 'role', 'type', 'tool', 'session', 'sid', 'parent', 'rollup', 'format', 'sort', 'unsloth', 'unsloth-format', 'exclude-sess', 'exclude-sid', 'exclude-cwd', 'exclude-project'],
30
30
  multi: ['grep', 'igrep', 'role', 'type', 'tool', 'session', 'sid', 'project', 'cwd', 'exclude-sess', 'exclude-sid', 'exclude-cwd', 'exclude-project'],
31
31
  number: ['limit', 'head', 'tail-n', 'ctx', 'truncate'],
32
- bool: ['json', 'ndjson', 'tail', 'f', 'full', 'reverse', 'invert', 'no-subagents', 'only-subagents', 'no-meta', 'only-meta', 'list-sessions', 'list-projects', 'list-tools', 'bash-discipline', 'include-subagents', 'stats', 'count', 'help', 'h'],
32
+ bool: ['json', 'ndjson', 'tail', 'f', 'full', 'reverse', 'invert', 'no-subagents', 'only-subagents', 'no-meta', 'only-meta', 'list-sessions', 'list-projects', 'list-tools', 'bash-discipline', 'git-discipline', 'include-subagents', 'stats', 'count', 'help', 'h'],
33
33
  };
34
34
 
35
35
  function parseArgs(argv) {
@@ -63,6 +63,7 @@ USAGE
63
63
  ccsniff --list-projects
64
64
  ccsniff --list-tools
65
65
  ccsniff --bash-discipline [--stats] Bash calls that should have used Read/Glob/Grep
66
+ ccsniff --git-discipline [--stats] git push from a dirty/unwitnessed tree
66
67
  (excludes subagents by default — --include-subagents to opt in;
67
68
  excludes 'echo > .gm/exec-spool/in/...' as canonical spool-write)
68
69
  ccsniff --stats [filters]
@@ -309,6 +310,48 @@ if (opts['bash-discipline']) {
309
310
  process.exit(0);
310
311
  }
311
312
 
313
+ if (opts['git-discipline']) {
314
+ const includeSubagents = opts['include-subagents'];
315
+ const PUSH = /\bgit\s+push\b/;
316
+ const PORCELAIN_CLEAN = /\bgit\s+status\s+(--porcelain|-s)\b/;
317
+ const bySid = new Map();
318
+ for (const ev of all) {
319
+ if (!filter(ev)) continue;
320
+ if (ev.block?.type !== 'tool_use' || ev.block?.name !== 'Bash') continue;
321
+ if (!includeSubagents && ev.conversation?.isSubagent) continue;
322
+ const sid = ev.conversation.id;
323
+ if (!bySid.has(sid)) bySid.set(sid, []);
324
+ bySid.get(sid).push(ev);
325
+ }
326
+ const violations = [];
327
+ for (const [sid, evs] of bySid) {
328
+ evs.sort((a, b) => a.timestamp - b.timestamp);
329
+ for (let i = 0; i < evs.length; i++) {
330
+ const ev = evs[i];
331
+ const cmd = ev.block?.input?.command || '';
332
+ if (!PUSH.test(cmd)) continue;
333
+ const lookback = evs.slice(Math.max(0, i - 20), i);
334
+ const witnessed = lookback.some(e => PORCELAIN_CLEAN.test(e.block?.input?.command || ''));
335
+ if (witnessed) continue;
336
+ violations.push({ ts: ev.timestamp, sid, project: path.basename(ev.conversation.cwd || ''), kind: 'push-no-porcelain-witness', cmd: cmd.slice(0, 200) });
337
+ }
338
+ }
339
+ if (opts.stats || opts.count) {
340
+ if (opts.count) { process.stdout.write(`${violations.length}\n`); process.exit(0); }
341
+ process.stdout.write(`# ${violations.length} git-discipline violations\n`);
342
+ const byProj = new Map();
343
+ for (const v of violations) byProj.set(v.project, (byProj.get(v.project) || 0) + 1);
344
+ process.stdout.write(`# by project\n`);
345
+ for (const [p, c] of [...byProj.entries()].sort((a, b) => b[1] - a[1])) process.stdout.write(` ${String(c).padStart(6)} ${p}\n`);
346
+ process.exit(0);
347
+ }
348
+ for (const v of violations) {
349
+ process.stdout.write(`${new Date(v.ts).toISOString().slice(0, 19)} ${v.sid.slice(0, 8)} ${v.kind.padEnd(28)} [${v.project}] ${v.cmd}\n`);
350
+ }
351
+ process.stderr.write(`# ${violations.length} violations (push-no-porcelain-witness)\n`);
352
+ process.exit(0);
353
+ }
354
+
312
355
  // ---------- list-tools
313
356
  if (opts['list-tools']) {
314
357
  const tools = new Map();