@deftai/directive-core 0.89.0 → 0.90.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.
Files changed (43) hide show
  1. package/dist/doctor/constants.d.ts +1 -1
  2. package/dist/doctor/constants.js +2 -0
  3. package/dist/doctor/flags.js +21 -1
  4. package/dist/doctor/index.d.ts +1 -0
  5. package/dist/doctor/index.js +1 -0
  6. package/dist/doctor/main.js +22 -0
  7. package/dist/doctor/openclaw-skills.d.ts +109 -0
  8. package/dist/doctor/openclaw-skills.js +463 -0
  9. package/dist/doctor/types.d.ts +11 -0
  10. package/dist/hooks/dispatcher.js +20 -5
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.js +1 -0
  13. package/dist/lifecycle/events.js +2 -0
  14. package/dist/lifecycle/index.d.ts +1 -0
  15. package/dist/lifecycle/index.js +1 -0
  16. package/dist/lifecycle/stats.d.ts +53 -0
  17. package/dist/lifecycle/stats.js +286 -0
  18. package/dist/release/spawn.js +13 -0
  19. package/dist/release-e2e/git-ops.d.ts +7 -0
  20. package/dist/release-e2e/git-ops.js +35 -2
  21. package/dist/session/index.d.ts +2 -0
  22. package/dist/session/index.js +2 -0
  23. package/dist/session/process-cost-constants.d.ts +9 -0
  24. package/dist/session/process-cost-constants.js +11 -0
  25. package/dist/session/process-cost.d.ts +53 -0
  26. package/dist/session/process-cost.js +82 -0
  27. package/dist/session/ritual-sentinel.d.ts +5 -0
  28. package/dist/session/ritual-sentinel.js +12 -1
  29. package/dist/session/session-ready.d.ts +67 -0
  30. package/dist/session/session-ready.js +264 -0
  31. package/dist/session/session-start.d.ts +56 -1
  32. package/dist/session/session-start.js +378 -24
  33. package/dist/session/verify-session-ritual.d.ts +8 -0
  34. package/dist/session/verify-session-ritual.js +95 -35
  35. package/dist/tool-events/classify.d.ts +20 -0
  36. package/dist/tool-events/classify.js +634 -0
  37. package/dist/tool-events/index.d.ts +10 -0
  38. package/dist/tool-events/index.js +10 -0
  39. package/dist/tool-events/summarize.d.ts +34 -0
  40. package/dist/tool-events/summarize.js +93 -0
  41. package/dist/tool-events/types.d.ts +52 -0
  42. package/dist/tool-events/types.js +13 -0
  43. package/package.json +7 -3
@@ -0,0 +1,634 @@
1
+ /**
2
+ * Deterministic, rule-first tool-event classifier (#2967).
3
+ *
4
+ * Pure: no I/O, no LLM. Misclassification policy: prefer `unknown` over a
5
+ * wrong `verify` (false-positive verify is worse than residual unknown).
6
+ *
7
+ * Distinct from packages/core/src/hooks/classify/ (#2950 write-intent/payload).
8
+ */
9
+ function normalizeName(name) {
10
+ return name
11
+ .trim()
12
+ .toLowerCase()
13
+ .replace(/[^a-z0-9]/g, "");
14
+ }
15
+ /** O(n) whitespace tokenize — no nested-quantifier regex on untrusted input. */
16
+ function shellTokens(command) {
17
+ const out = [];
18
+ let cur = "";
19
+ for (let i = 0; i < command.length; i++) {
20
+ const c = command[i];
21
+ if (c === undefined)
22
+ break;
23
+ if (c === " " || c === "\t" || c === "\n" || c === "\r") {
24
+ if (cur.length > 0) {
25
+ out.push(cur);
26
+ cur = "";
27
+ }
28
+ continue;
29
+ }
30
+ cur += c;
31
+ }
32
+ if (cur.length > 0)
33
+ out.push(cur);
34
+ return out;
35
+ }
36
+ function normalizeToken(token) {
37
+ // O(n) character-class strip (quotes only) — avoid anchored `+` quantifiers
38
+ // (CodeQL js/polynomial-redos). Path separators stay for stripPathNoise.
39
+ return token.replace(/['"`]/g, "").toLowerCase();
40
+ }
41
+ function stripPathNoise(token) {
42
+ const n = normalizeToken(token);
43
+ // basename-ish: last path segment, drop .exe
44
+ const slash = Math.max(n.lastIndexOf("/"), n.lastIndexOf("\\"));
45
+ const base = slash >= 0 ? n.slice(slash + 1) : n;
46
+ return base.endsWith(".exe") ? base.slice(0, -4) : base;
47
+ }
48
+ function isEnvAssign(token) {
49
+ const eq = token.indexOf("=");
50
+ if (eq <= 0)
51
+ return false;
52
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(token.slice(0, eq));
53
+ }
54
+ function skipWrappers(tokens) {
55
+ let i = 0;
56
+ while (i < tokens.length && isEnvAssign(tokens[i]))
57
+ i++;
58
+ const wrap = tokens[i] !== undefined ? stripPathNoise(tokens[i]) : "";
59
+ if (wrap === "sudo" || wrap === "env" || wrap === "command" || wrap === "time") {
60
+ i++;
61
+ while (i < tokens.length && isEnvAssign(tokens[i]))
62
+ i++;
63
+ }
64
+ return i;
65
+ }
66
+ function fieldString(args, key) {
67
+ if (args == null)
68
+ return null;
69
+ const v = args[key];
70
+ if (typeof v === "string" && v.trim().length > 0)
71
+ return v;
72
+ return null;
73
+ }
74
+ function resolveCommand(event) {
75
+ if (typeof event.command === "string" && event.command.trim().length > 0) {
76
+ return event.command;
77
+ }
78
+ const fromArgs = fieldString(event.args, "command") ??
79
+ fieldString(event.args, "cmd") ??
80
+ fieldString(event.args, "script");
81
+ return fromArgs;
82
+ }
83
+ // ---------------------------------------------------------------------------
84
+ // Name-only sets (normalized: lowercase, non-alnum stripped)
85
+ // ---------------------------------------------------------------------------
86
+ const EXPLORE_NAMES = new Set([
87
+ "read",
88
+ "readfile",
89
+ "read_file",
90
+ "grep",
91
+ "rg",
92
+ "glob",
93
+ "globfile",
94
+ "listdir",
95
+ "list_dir",
96
+ "listfiles",
97
+ "ls",
98
+ "search",
99
+ "semanticsearch",
100
+ "codebase_search",
101
+ "codebasesearch",
102
+ "webfetch",
103
+ "web_fetch",
104
+ "websearch",
105
+ "web_search",
106
+ "openpage",
107
+ "open_page",
108
+ "browse",
109
+ "fetch",
110
+ "cat",
111
+ "head",
112
+ "tail",
113
+ "find",
114
+ "astgrep",
115
+ "ast_grep",
116
+ "getdiagnostics",
117
+ "get_diagnostics",
118
+ "readlints",
119
+ "read_lints",
120
+ ].map(normalizeName));
121
+ const COMMIT_NAMES = new Set([
122
+ "write",
123
+ "writefile",
124
+ "write_file",
125
+ "createfile",
126
+ "create_file",
127
+ "edit",
128
+ "streplace",
129
+ "str_replace",
130
+ "searchreplace",
131
+ "search_replace",
132
+ "multiedit",
133
+ "multi_edit",
134
+ "notebookedit",
135
+ "notebook_edit",
136
+ "applypatch",
137
+ "apply_patch",
138
+ "delete",
139
+ "deletefile",
140
+ "delete_file",
141
+ "editnotebook",
142
+ "inserteditinto",
143
+ ].map(normalizeName));
144
+ const COORDINATE_NAMES = new Set([
145
+ "task",
146
+ "subagentstart",
147
+ "subagent_start",
148
+ "spawnsubagent",
149
+ "spawn_subagent",
150
+ "startagent",
151
+ "start_agent",
152
+ "createagent",
153
+ "create_agent",
154
+ "sessionspawn",
155
+ "sessions_spawn",
156
+ "askuserquestion",
157
+ "ask_user_question",
158
+ "askquestion",
159
+ "todowrite",
160
+ "todo_write",
161
+ "todoread",
162
+ "todo_read",
163
+ "switchmode",
164
+ "switch_mode",
165
+ "sendmessage",
166
+ "send_message",
167
+ "message",
168
+ "wait",
169
+ "sleep",
170
+ ].map(normalizeName));
171
+ /** Names that are always verify without needing args. */
172
+ const VERIFY_NAMES = new Set(["runtests", "run_tests", "testrunner", "test_runner"].map(normalizeName));
173
+ const SHELL_NAMES = new Set([
174
+ "shell",
175
+ "bash",
176
+ "bashtool",
177
+ "runterminalcommand",
178
+ "run_terminal_command",
179
+ "run_terminal_cmd",
180
+ ].map(normalizeName));
181
+ // ---------------------------------------------------------------------------
182
+ // Shell command classification (strict verify — prefer unknown)
183
+ // ---------------------------------------------------------------------------
184
+ /** Bins that are verify when alone or with known verify subcommands. */
185
+ const VERIFY_BINS_ALONE = new Set([
186
+ "vitest",
187
+ "pytest",
188
+ "jest",
189
+ "mocha",
190
+ "eslint",
191
+ "biome",
192
+ "prettier",
193
+ "tsc",
194
+ "mypy",
195
+ "ruff",
196
+ "golangci-lint",
197
+ "golangci_lint",
198
+ ]);
199
+ /** task / deft / directive verify-ish verbs (second token). */
200
+ const VERIFY_TASK_VERBS = new Set([
201
+ "check",
202
+ "test",
203
+ "doctor",
204
+ "lint",
205
+ "typecheck",
206
+ "verify",
207
+ "verify:encoding",
208
+ "verify:branch",
209
+ "verify:tools",
210
+ "verify:session-ritual",
211
+ "verify:cache-fresh",
212
+ "verify:forward-coverage",
213
+ "verify:story-ready",
214
+ "verify:review-monitor",
215
+ "pr:watch",
216
+ "pr:merge-ready",
217
+ "coverage:hotspots",
218
+ ]);
219
+ const VERIFY_NPM_SCRIPTS = new Set([
220
+ "test",
221
+ "lint",
222
+ "typecheck",
223
+ "type-check",
224
+ "check",
225
+ "ci",
226
+ "verify",
227
+ ]);
228
+ const EXPLORE_GIT_SUB = new Set([
229
+ "status",
230
+ "log",
231
+ "diff",
232
+ "show",
233
+ "blame",
234
+ "branch",
235
+ "rev-parse",
236
+ "rev_parse",
237
+ "ls-files",
238
+ "ls_files",
239
+ "describe",
240
+ "remote",
241
+ "stash", // list-ish; stash push is commit — see below
242
+ "cat-file",
243
+ "cat_file",
244
+ "shortlog",
245
+ "whatchanged",
246
+ ]);
247
+ const COMMIT_GIT_SUB = new Set([
248
+ "add",
249
+ "commit",
250
+ "push",
251
+ "pull", // mutates working tree / refs — treat as commit-class mutator
252
+ "checkout",
253
+ "switch",
254
+ "merge",
255
+ "rebase",
256
+ "reset",
257
+ "rm",
258
+ "mv",
259
+ "cherry-pick",
260
+ "cherry_pick",
261
+ "revert",
262
+ "tag",
263
+ "am",
264
+ "apply",
265
+ "clean",
266
+ ]);
267
+ const EXPLORE_BINS = new Set([
268
+ "cat",
269
+ "head",
270
+ "tail",
271
+ "less",
272
+ "more",
273
+ "ls",
274
+ "dir",
275
+ "find",
276
+ "rg",
277
+ "grep",
278
+ "ag",
279
+ "ack",
280
+ "fd",
281
+ "tree",
282
+ "wc",
283
+ "file",
284
+ "stat",
285
+ "which",
286
+ "where",
287
+ "type",
288
+ "echo",
289
+ "pwd",
290
+ "printenv",
291
+ "env",
292
+ "jq",
293
+ "yq",
294
+ "bat",
295
+ "sed", // read-ish pipes often explore; mutators hard to prove — leave alone only
296
+ "awk",
297
+ "sort",
298
+ "uniq",
299
+ "diff",
300
+ "cmp",
301
+ "hexdump",
302
+ "od",
303
+ "strings",
304
+ "man",
305
+ "help",
306
+ "ghx", // cached read-only gh proxy
307
+ ]);
308
+ const COMMIT_BINS = new Set([
309
+ "rm",
310
+ "mv",
311
+ "cp",
312
+ "mkdir",
313
+ "touch",
314
+ "tee",
315
+ "install",
316
+ "chmod",
317
+ "chown",
318
+ "ln",
319
+ "sed", // when not clearly a pure pipe — we only match standalone sed as unknown; see rules
320
+ ]);
321
+ function result(bucket, reason) {
322
+ return { bucket, reason };
323
+ }
324
+ /**
325
+ * Classify a shell command into a bucket.
326
+ * Verify is intentionally narrow (prefer unknown over wrong verify).
327
+ */
328
+ function classifyShellCommand(command) {
329
+ const trimmed = command.trim();
330
+ if (trimmed.length === 0) {
331
+ return result("unknown", "shell-empty-command");
332
+ }
333
+ const tokens = shellTokens(trimmed);
334
+ const i = skipWrappers(tokens);
335
+ const binRaw = tokens[i];
336
+ if (binRaw === undefined) {
337
+ return result("unknown", "shell-no-bin");
338
+ }
339
+ const bin = stripPathNoise(binRaw);
340
+ const rest = tokens.slice(i + 1);
341
+ const second = rest[0] !== undefined ? normalizeToken(rest[0]) : "";
342
+ const secondBare = stripPathNoise(rest[0] ?? "");
343
+ // --- verify (strict) ---
344
+ if (VERIFY_BINS_ALONE.has(bin)) {
345
+ return result("verify", `shell-verify-bin:${bin}`);
346
+ }
347
+ if (bin === "go" && secondBare === "test") {
348
+ return result("verify", "shell-verify-go-test");
349
+ }
350
+ if (bin === "cargo" &&
351
+ (secondBare === "test" || secondBare === "clippy" || secondBare === "check")) {
352
+ return result("verify", `shell-verify-cargo-${secondBare}`);
353
+ }
354
+ if ((bin === "npm" || bin === "pnpm" || bin === "yarn" || bin === "bun") &&
355
+ (secondBare === "test" ||
356
+ secondBare === "run" ||
357
+ secondBare === "exec" ||
358
+ secondBare === "dlx" ||
359
+ secondBare === "x")) {
360
+ // npm test / pnpm test
361
+ if (secondBare === "test") {
362
+ return result("verify", `shell-verify-pm-test:${bin}`);
363
+ }
364
+ // npm run <script> — only known verify scripts
365
+ const script = rest[1] !== undefined ? stripPathNoise(rest[1]) : "";
366
+ if (script.length > 0 && VERIFY_NPM_SCRIPTS.has(script)) {
367
+ return result("verify", `shell-verify-pm-script:${bin}:${script}`);
368
+ }
369
+ // vitest / eslint invoked via pnpm exec
370
+ if (script.length > 0 && VERIFY_BINS_ALONE.has(script)) {
371
+ return result("verify", `shell-verify-pm-exec:${bin}:${script}`);
372
+ }
373
+ // secondBare is run|exec|dlx|x here (test returned above) — residual unknown
374
+ return result("unknown", `shell-pm-unknown-script:${bin}:${script || "?"}`);
375
+ }
376
+ if (bin === "npx" || bin === "pnpx") {
377
+ const tool = secondBare;
378
+ if (tool.length > 0 && VERIFY_BINS_ALONE.has(tool)) {
379
+ return result("verify", `shell-verify-npx:${tool}`);
380
+ }
381
+ return result("unknown", `shell-npx-unknown:${tool || "?"}`);
382
+ }
383
+ if (bin === "task" || bin === "deft" || bin === "directive") {
384
+ // task check / task verify:encoding / deft doctor / task pr:watch
385
+ if (second.length > 0) {
386
+ const verb = second.replace(/^--+/, "");
387
+ if (VERIFY_TASK_VERBS.has(verb)) {
388
+ return result("verify", `shell-verify-task:${bin}:${verb}`);
389
+ }
390
+ // task verify:* / check:* namespaces
391
+ if (verb.startsWith("verify:") || verb.startsWith("check:") || verb === "doctor") {
392
+ return result("verify", `shell-verify-task-ns:${bin}:${verb}`);
393
+ }
394
+ // task test → verify
395
+ if (verb === "test" || verb.startsWith("test:")) {
396
+ return result("verify", `shell-verify-task-test:${bin}:${verb}`);
397
+ }
398
+ }
399
+ // Other task verbs → not auto-verify (could be mutate)
400
+ // Fall through to commit/explore heuristics for known mutators
401
+ }
402
+ if (bin === "python" || bin === "python3" || bin === "py") {
403
+ // python -m pytest only
404
+ for (let j = 0; j < rest.length - 1; j++) {
405
+ if (normalizeToken(rest[j]) === "-m" &&
406
+ stripPathNoise(rest[j + 1]) === "pytest") {
407
+ return result("verify", "shell-verify-python-pytest");
408
+ }
409
+ }
410
+ return result("unknown", "shell-python-unknown");
411
+ }
412
+ if (bin === "node") {
413
+ // node --test is verify; bare node scripts are unknown
414
+ if (rest.some((t) => normalizeToken(t) === "--test")) {
415
+ return result("verify", "shell-verify-node-test");
416
+ }
417
+ return result("unknown", "shell-node-unknown");
418
+ }
419
+ if (bin === "make") {
420
+ // make test / make check / make lint only
421
+ if (secondBare === "test" ||
422
+ secondBare === "check" ||
423
+ secondBare === "lint" ||
424
+ secondBare === "typecheck" ||
425
+ secondBare === "verify") {
426
+ return result("verify", `shell-verify-make:${secondBare}`);
427
+ }
428
+ return result("unknown", `shell-make-unknown:${secondBare || "?"}`);
429
+ }
430
+ // --- git ---
431
+ if (bin === "git") {
432
+ const sub = secondBare;
433
+ if (sub === "stash") {
434
+ // git stash (list) vs git stash push/pop/apply
435
+ const third = rest[1] !== undefined ? stripPathNoise(rest[1]) : "";
436
+ if (third === "push" ||
437
+ third === "pop" ||
438
+ third === "apply" ||
439
+ third === "drop" ||
440
+ third === "clear" ||
441
+ third === "create" ||
442
+ third === "store") {
443
+ return result("commit", `shell-git-mutator:stash-${third}`);
444
+ }
445
+ // bare stash / stash list / stash show → explore
446
+ return result("explore", "shell-git-explore:stash");
447
+ }
448
+ if (COMMIT_GIT_SUB.has(sub)) {
449
+ return result("commit", `shell-git-mutator:${sub}`);
450
+ }
451
+ if (EXPLORE_GIT_SUB.has(sub) || sub === "") {
452
+ return result("explore", `shell-git-explore:${sub || "default"}`);
453
+ }
454
+ // Unknown git subcommand → unknown (not verify)
455
+ return result("unknown", `shell-git-unknown:${sub || "?"}`);
456
+ }
457
+ // --- gh / ghx (read vs mutate) ---
458
+ if (bin === "gh" || bin === "gh.exe") {
459
+ // gh api GET-ish: treat as explore unless method is clearly write
460
+ const sub = secondBare;
461
+ if (sub === "api") {
462
+ const joined = rest.map((t) => normalizeToken(t)).join(" ");
463
+ if (joined.includes("-x post") ||
464
+ joined.includes("-x put") ||
465
+ joined.includes("-x patch") ||
466
+ joined.includes("-x delete") ||
467
+ joined.includes("--method post") ||
468
+ joined.includes("--method put") ||
469
+ joined.includes("--method patch") ||
470
+ joined.includes("--method delete")) {
471
+ return result("commit", "shell-gh-api-mutate");
472
+ }
473
+ return result("explore", "shell-gh-api-read");
474
+ }
475
+ if (sub === "pr" ||
476
+ sub === "issue" ||
477
+ sub === "repo" ||
478
+ sub === "release" ||
479
+ sub === "workflow") {
480
+ const verb = rest[1] !== undefined ? stripPathNoise(rest[1]) : "";
481
+ const readVerbs = new Set([
482
+ "view",
483
+ "list",
484
+ "status",
485
+ "checks",
486
+ "diff",
487
+ "files",
488
+ "log",
489
+ "browse",
490
+ ]);
491
+ if (readVerbs.has(verb)) {
492
+ return result("explore", `shell-gh-read:${sub}:${verb}`);
493
+ }
494
+ if (verb.length > 0) {
495
+ // create, edit, merge, close, comment, … → commit-class mutator
496
+ return result("commit", `shell-gh-mutate:${sub}:${verb}`);
497
+ }
498
+ return result("unknown", `shell-gh-incomplete:${sub}`);
499
+ }
500
+ if (sub === "auth" || sub === "config" || sub === "help" || sub === "version") {
501
+ return result("explore", `shell-gh-meta:${sub}`);
502
+ }
503
+ return result("unknown", `shell-gh-unknown:${sub || "?"}`);
504
+ }
505
+ if (bin === "ghx") {
506
+ return result("explore", "shell-ghx-read");
507
+ }
508
+ // --- explore bins ---
509
+ if (EXPLORE_BINS.has(bin)) {
510
+ // sed/awk often in pipes for explore; standalone sed -i would be commit — detect -i
511
+ if (bin === "sed") {
512
+ if (rest.some((t) => normalizeToken(t) === "-i" || normalizeToken(t).startsWith("-i"))) {
513
+ return result("commit", "shell-sed-inplace");
514
+ }
515
+ return result("explore", "shell-explore-sed");
516
+ }
517
+ return result("explore", `shell-explore-bin:${bin}`);
518
+ }
519
+ // --- filesystem mutators ---
520
+ if (bin === "rm" || bin === "mv" || bin === "cp" || bin === "mkdir" || bin === "touch") {
521
+ return result("commit", `shell-fs-mutate:${bin}`);
522
+ }
523
+ if (COMMIT_BINS.has(bin) && bin !== "sed") {
524
+ return result("commit", `shell-commit-bin:${bin}`);
525
+ }
526
+ // --- coordinate-ish shell (swarm launch etc.) ---
527
+ if (bin === "task" || bin === "deft" || bin === "directive") {
528
+ const verb = second.replace(/^--+/, "");
529
+ if (verb.startsWith("swarm:") ||
530
+ verb.startsWith("scope:") ||
531
+ verb.startsWith("session:") ||
532
+ verb === "swarm" ||
533
+ verb === "scope") {
534
+ return result("coordinate", `shell-coordinate-task:${verb}`);
535
+ }
536
+ // residual task verbs
537
+ return result("unknown", `shell-task-unknown:${verb || "?"}`);
538
+ }
539
+ // Prefer unknown over inventing verify/commit
540
+ return result("unknown", `shell-unknown-bin:${bin}`);
541
+ }
542
+ /**
543
+ * Classify one tool event into explore | commit | verify | coordinate | unknown.
544
+ *
545
+ * Pure and deterministic. When verify cannot be proven honestly, returns unknown.
546
+ */
547
+ export function classifyToolEvent(event) {
548
+ const rawName = typeof event.name === "string" ? event.name : "";
549
+ if (rawName.trim().length === 0) {
550
+ return result("unknown", "missing-name");
551
+ }
552
+ const name = normalizeName(rawName);
553
+ if (EXPLORE_NAMES.has(name)) {
554
+ return result("explore", `name-explore:${name}`);
555
+ }
556
+ if (COMMIT_NAMES.has(name)) {
557
+ return result("commit", `name-commit:${name}`);
558
+ }
559
+ if (COORDINATE_NAMES.has(name)) {
560
+ return result("coordinate", `name-coordinate:${name}`);
561
+ }
562
+ if (VERIFY_NAMES.has(name)) {
563
+ return result("verify", `name-verify:${name}`);
564
+ }
565
+ // Shell-class tools: classify from command when present
566
+ if (SHELL_NAMES.has(name) ||
567
+ name.includes("shell") ||
568
+ name.includes("bash") ||
569
+ name.includes("terminal")) {
570
+ const cmd = resolveCommand(event);
571
+ if (cmd === null) {
572
+ // Shell without command cannot prove verify (or anything else)
573
+ return result("unknown", "shell-missing-command");
574
+ }
575
+ return classifyShellCommand(cmd);
576
+ }
577
+ // MCP-style names: server__tool — try last segment as name
578
+ if (rawName.includes("__") || rawName.includes("/")) {
579
+ const parts = rawName.split(/__|\//);
580
+ const last = parts[parts.length - 1] ?? "";
581
+ const lastNorm = normalizeName(last);
582
+ if (lastNorm.length > 0 && lastNorm !== name) {
583
+ const nested = classifyToolEvent({ name: last, args: event.args, command: event.command });
584
+ if (nested.bucket !== "unknown") {
585
+ return result(nested.bucket, `mcp-nested:${nested.reason}`);
586
+ }
587
+ }
588
+ // MCP without nested match
589
+ return result("unknown", `mcp-unknown:${name}`);
590
+ }
591
+ // Coarse name heuristics (still prefer unknown for verify).
592
+ // Coordinate before commit: "dispatch" contains "patch" as a substring.
593
+ if (name.includes("read") ||
594
+ name.includes("grep") ||
595
+ name.includes("search") ||
596
+ name.includes("list") ||
597
+ name.includes("glob") ||
598
+ name.includes("fetch") ||
599
+ name.includes("browse")) {
600
+ return result("explore", `heuristic-explore:${name}`);
601
+ }
602
+ if (name.includes("spawn") ||
603
+ name.includes("subagent") ||
604
+ name.includes("agent") ||
605
+ name.includes("todo") ||
606
+ name.includes("message") ||
607
+ name.includes("dispatch")) {
608
+ return result("coordinate", `heuristic-coordinate:${name}`);
609
+ }
610
+ if (name.includes("write") ||
611
+ name.includes("edit") ||
612
+ name.includes("patch") ||
613
+ name.includes("delete") ||
614
+ name.includes("create") ||
615
+ name.includes("apply")) {
616
+ // createagent / startagent already handled in COORDINATE_NAMES and above
617
+ return result("commit", `heuristic-commit:${name}`);
618
+ }
619
+ // ⊗ Do not heuristic-match "test"/"check"/"verify" in bare names alone —
620
+ // that is the false-positive verify class we forbid.
621
+ if (name.includes("test") || name.includes("lint") || name.includes("typecheck")) {
622
+ return result("unknown", `ambiguous-verify-name:${name}`);
623
+ }
624
+ return result("unknown", `residual:${name}`);
625
+ }
626
+ /** Classify many events; order preserved. */
627
+ export function classifyToolEvents(events) {
628
+ return events.map((e) => classifyToolEvent(e));
629
+ }
630
+ /** Exported for unit tests of the shell path without wrapping Shell tool names. */
631
+ export function classifyShellCommandForTest(command) {
632
+ return classifyShellCommand(command);
633
+ }
634
+ //# sourceMappingURL=classify.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Tool-event taxonomy: deterministic explore/commit/verify classifier (#2967).
3
+ *
4
+ * Public surface for swarm/review consumers. Pure — no I/O, no LLM.
5
+ * Taxonomy docs: content/patterns/tool-call-taxonomy.md
6
+ */
7
+ export { classifyShellCommandForTest, classifyToolEvent, classifyToolEvents, } from "./classify.js";
8
+ export { countToolEventBuckets, detectToolEventAnomalies, emptyBucketCounts, formatToolEventAnomalyLine, formatToolEventStatusLine, summarizeToolEvents, } from "./summarize.js";
9
+ export { type ClassifyToolEventResult, TOOL_EVENT_BUCKETS, type ToolEventAnomaly, type ToolEventAnomalyCode, type ToolEventBucket, type ToolEventBucketCounts, type ToolEventInput, type ToolEventSummary, } from "./types.js";
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Tool-event taxonomy: deterministic explore/commit/verify classifier (#2967).
3
+ *
4
+ * Public surface for swarm/review consumers. Pure — no I/O, no LLM.
5
+ * Taxonomy docs: content/patterns/tool-call-taxonomy.md
6
+ */
7
+ export { classifyShellCommandForTest, classifyToolEvent, classifyToolEvents, } from "./classify.js";
8
+ export { countToolEventBuckets, detectToolEventAnomalies, emptyBucketCounts, formatToolEventAnomalyLine, formatToolEventStatusLine, summarizeToolEvents, } from "./summarize.js";
9
+ export { TOOL_EVENT_BUCKETS, } from "./types.js";
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Aggregate tool-event classifications into counts, anomalies, and status lines (#2967).
3
+ * Pure: no I/O.
4
+ */
5
+ import { type ToolEventAnomaly, type ToolEventBucketCounts, type ToolEventInput, type ToolEventSummary } from "./types.js";
6
+ export declare function emptyBucketCounts(): ToolEventBucketCounts;
7
+ /** Count classified buckets for a sequence of tool events. */
8
+ export declare function countToolEventBuckets(events: readonly ToolEventInput[]): ToolEventBucketCounts;
9
+ /**
10
+ * Detect operator-facing anomalies from bucket counts.
11
+ *
12
+ * Rules (conservative — only fire when the mix is clearly wrong):
13
+ * - commit-without-explore: commit > 0 and explore === 0
14
+ * - verify-skipped: commit > 0 and verify === 0 (ship without gates)
15
+ * - explore-only: explore > 0, commit === 0, verify === 0, and total >= 3
16
+ * (thrash / stuck-reading signal; small explore-only sessions stay quiet)
17
+ */
18
+ export declare function detectToolEventAnomalies(counts: ToolEventBucketCounts): ToolEventAnomaly[];
19
+ /**
20
+ * Compact status line for swarm monitor / review-cycle batch brief.
21
+ * Example: `tools: explore=3 commit=2 verify=1 coordinate=0 unknown=0`
22
+ */
23
+ export declare function formatToolEventStatusLine(counts: ToolEventBucketCounts): string;
24
+ /**
25
+ * One-line anomaly suffix for status surfaces.
26
+ * Empty string when no anomalies.
27
+ */
28
+ export declare function formatToolEventAnomalyLine(anomalies: readonly ToolEventAnomaly[]): string;
29
+ /**
30
+ * Full summary for a tool-event sequence — counts, anomalies, status line.
31
+ * When anomalies exist, statusLine appends `| anomalies: …`.
32
+ */
33
+ export declare function summarizeToolEvents(events: readonly ToolEventInput[]): ToolEventSummary;
34
+ //# sourceMappingURL=summarize.d.ts.map