ccsniff 1.1.2 → 1.1.4
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 +39 -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,27 @@ 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\//;
|
|
251
|
+
// The host harness explicitly endorses `until <check>; do sleep N; done` as
|
|
252
|
+
// the canonical pattern for polling external state (see Bash tool description
|
|
253
|
+
// and Monitor docs). Same for `while !curl ...; do sleep N; done`. These are
|
|
254
|
+
// NOT sleep-poll violations even though they contain `sleep N`.
|
|
255
|
+
const ENDORSED_POLL = /^\s*(until|while)\s+/;
|
|
245
256
|
const violations = [];
|
|
246
257
|
for (const ev of all) {
|
|
247
258
|
if (!filter(ev)) continue;
|
|
248
259
|
if (ev.block?.type !== 'tool_use' || ev.block?.name !== 'Bash') continue;
|
|
260
|
+
if (!includeSubagents && ev.conversation?.isSubagent) continue;
|
|
249
261
|
const cmd = ev.block?.input?.command || '';
|
|
262
|
+
// `echo > .gm/exec-spool/in/<verb>/N.txt` is the canonical spool-write pattern, not a deviation.
|
|
263
|
+
if (SPOOL_WRITE.test(cmd) && /^\s*echo\b/.test(cmd)) continue;
|
|
264
|
+
// `until ...; do sleep N; done` is the harness-endorsed poll pattern.
|
|
265
|
+
if (ENDORSED_POLL.test(cmd)) continue;
|
|
250
266
|
const kind = SLEEP_POLL.test(cmd) ? 'sleep-poll' : (BAD_LEADING.test(cmd) ? 'bad-leading-cmd' : null);
|
|
251
267
|
if (!kind) continue;
|
|
252
268
|
violations.push({ ts: ev.timestamp, sid: ev.conversation.id, project: path.basename(ev.conversation.cwd || ''), kind, cmd: cmd.slice(0, 200) });
|
|
@@ -255,12 +271,32 @@ if (opts['bash-discipline']) {
|
|
|
255
271
|
for (const v of violations) byKind.set(v.kind, (byKind.get(v.kind) || 0) + 1);
|
|
256
272
|
if (opts.stats || opts.count) {
|
|
257
273
|
if (opts.count) { process.stdout.write(`${violations.length}\n`); process.exit(0); }
|
|
258
|
-
|
|
274
|
+
const subagentNote = includeSubagents ? '' : ' (subagents excluded — pass --include-subagents to include)';
|
|
275
|
+
process.stdout.write(`# ${violations.length} bash-discipline violations${subagentNote}\n`);
|
|
259
276
|
for (const [k, c] of [...byKind.entries()].sort((a, b) => b[1] - a[1])) process.stdout.write(` ${String(c).padStart(6)} ${k}\n`);
|
|
260
277
|
const byProj = new Map();
|
|
261
278
|
for (const v of violations) byProj.set(v.project, (byProj.get(v.project) || 0) + 1);
|
|
262
279
|
process.stdout.write(`# by project\n`);
|
|
263
280
|
for (const [p, c] of [...byProj.entries()].sort((a, b) => b[1] - a[1])) process.stdout.write(` ${String(c).padStart(6)} ${p}\n`);
|
|
281
|
+
const byDay = new Map();
|
|
282
|
+
for (const v of violations) {
|
|
283
|
+
const day = new Date(v.ts).toISOString().slice(0, 10);
|
|
284
|
+
byDay.set(day, (byDay.get(day) || 0) + 1);
|
|
285
|
+
}
|
|
286
|
+
if (byDay.size > 1) {
|
|
287
|
+
process.stdout.write(`# by day\n`);
|
|
288
|
+
for (const [d, c] of [...byDay.entries()].sort((a, b) => a[0].localeCompare(b[0]))) process.stdout.write(` ${String(c).padStart(6)} ${d}\n`);
|
|
289
|
+
}
|
|
290
|
+
const byHour = new Map();
|
|
291
|
+
for (const v of violations) {
|
|
292
|
+
const hour = new Date(v.ts).toISOString().slice(0, 13);
|
|
293
|
+
byHour.set(hour, (byHour.get(hour) || 0) + 1);
|
|
294
|
+
}
|
|
295
|
+
if (byHour.size > 1) {
|
|
296
|
+
process.stdout.write(`# by hour (last 12)\n`);
|
|
297
|
+
const sorted = [...byHour.entries()].sort((a, b) => a[0].localeCompare(b[0])).slice(-12);
|
|
298
|
+
for (const [h, c] of sorted) process.stdout.write(` ${String(c).padStart(6)} ${h}:00\n`);
|
|
299
|
+
}
|
|
264
300
|
process.exit(0);
|
|
265
301
|
}
|
|
266
302
|
for (const v of violations) {
|