ccsniff 1.1.2 → 1.1.3
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/package.json +1 -1
- package/src/cli.js +32 -3
package/package.json
CHANGED
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'],
|
|
30
30
|
multi: ['grep', 'igrep', 'role', 'type', 'tool', 'session', 'sid', 'project', 'cwd'],
|
|
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', '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', 'include-subagents', 'stats', 'count', 'help', 'h'],
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
function parseArgs(argv) {
|
|
@@ -63,6 +63,8 @@ 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
|
+
(excludes subagents by default — --include-subagents to opt in;
|
|
67
|
+
excludes 'echo > .gm/exec-spool/in/...' as canonical spool-write)
|
|
66
68
|
ccsniff --stats [filters]
|
|
67
69
|
|
|
68
70
|
TIME (any ISO date, epoch ms, or relative Ns/Nm/Nh/Nd/Nw)
|
|
@@ -240,13 +242,20 @@ if (opts['list-projects']) {
|
|
|
240
242
|
|
|
241
243
|
// ---------- bash-discipline (flag Bash calls that should have been Read/Glob/Grep/dispatch)
|
|
242
244
|
if (opts['bash-discipline']) {
|
|
243
|
-
|
|
245
|
+
// discipline is about MY tool routing, not subagents — they have separate prompts/contexts.
|
|
246
|
+
// Default: exclude subagents. --include-subagents opts them back in.
|
|
247
|
+
const includeSubagents = opts['include-subagents'];
|
|
248
|
+
const BAD_LEADING = /^\s*(cat|head|tail|ls|grep|find|sed|awk)\b/;
|
|
244
249
|
const SLEEP_POLL = /\bsleep\s+\d+\s*;.*(cat|ls|grep|find|head|tail)/;
|
|
250
|
+
const SPOOL_WRITE = /\.gm\/exec-spool\/in\//;
|
|
245
251
|
const violations = [];
|
|
246
252
|
for (const ev of all) {
|
|
247
253
|
if (!filter(ev)) continue;
|
|
248
254
|
if (ev.block?.type !== 'tool_use' || ev.block?.name !== 'Bash') continue;
|
|
255
|
+
if (!includeSubagents && ev.conversation?.isSubagent) continue;
|
|
249
256
|
const cmd = ev.block?.input?.command || '';
|
|
257
|
+
// `echo > .gm/exec-spool/in/<verb>/N.txt` is the canonical spool-write pattern, not a deviation.
|
|
258
|
+
if (SPOOL_WRITE.test(cmd) && /^\s*echo\b/.test(cmd)) continue;
|
|
250
259
|
const kind = SLEEP_POLL.test(cmd) ? 'sleep-poll' : (BAD_LEADING.test(cmd) ? 'bad-leading-cmd' : null);
|
|
251
260
|
if (!kind) continue;
|
|
252
261
|
violations.push({ ts: ev.timestamp, sid: ev.conversation.id, project: path.basename(ev.conversation.cwd || ''), kind, cmd: cmd.slice(0, 200) });
|
|
@@ -255,12 +264,32 @@ if (opts['bash-discipline']) {
|
|
|
255
264
|
for (const v of violations) byKind.set(v.kind, (byKind.get(v.kind) || 0) + 1);
|
|
256
265
|
if (opts.stats || opts.count) {
|
|
257
266
|
if (opts.count) { process.stdout.write(`${violations.length}\n`); process.exit(0); }
|
|
258
|
-
|
|
267
|
+
const subagentNote = includeSubagents ? '' : ' (subagents excluded — pass --include-subagents to include)';
|
|
268
|
+
process.stdout.write(`# ${violations.length} bash-discipline violations${subagentNote}\n`);
|
|
259
269
|
for (const [k, c] of [...byKind.entries()].sort((a, b) => b[1] - a[1])) process.stdout.write(` ${String(c).padStart(6)} ${k}\n`);
|
|
260
270
|
const byProj = new Map();
|
|
261
271
|
for (const v of violations) byProj.set(v.project, (byProj.get(v.project) || 0) + 1);
|
|
262
272
|
process.stdout.write(`# by project\n`);
|
|
263
273
|
for (const [p, c] of [...byProj.entries()].sort((a, b) => b[1] - a[1])) process.stdout.write(` ${String(c).padStart(6)} ${p}\n`);
|
|
274
|
+
const byDay = new Map();
|
|
275
|
+
for (const v of violations) {
|
|
276
|
+
const day = new Date(v.ts).toISOString().slice(0, 10);
|
|
277
|
+
byDay.set(day, (byDay.get(day) || 0) + 1);
|
|
278
|
+
}
|
|
279
|
+
if (byDay.size > 1) {
|
|
280
|
+
process.stdout.write(`# by day\n`);
|
|
281
|
+
for (const [d, c] of [...byDay.entries()].sort((a, b) => a[0].localeCompare(b[0]))) process.stdout.write(` ${String(c).padStart(6)} ${d}\n`);
|
|
282
|
+
}
|
|
283
|
+
const byHour = new Map();
|
|
284
|
+
for (const v of violations) {
|
|
285
|
+
const hour = new Date(v.ts).toISOString().slice(0, 13);
|
|
286
|
+
byHour.set(hour, (byHour.get(hour) || 0) + 1);
|
|
287
|
+
}
|
|
288
|
+
if (byHour.size > 1) {
|
|
289
|
+
process.stdout.write(`# by hour (last 12)\n`);
|
|
290
|
+
const sorted = [...byHour.entries()].sort((a, b) => a[0].localeCompare(b[0])).slice(-12);
|
|
291
|
+
for (const [h, c] of sorted) process.stdout.write(` ${String(c).padStart(6)} ${h}:00\n`);
|
|
292
|
+
}
|
|
264
293
|
process.exit(0);
|
|
265
294
|
}
|
|
266
295
|
for (const v of violations) {
|