@deftai/directive-core 0.89.0 → 0.91.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.
- package/dist/doctor/constants.d.ts +1 -1
- package/dist/doctor/constants.js +2 -0
- package/dist/doctor/flags.js +21 -1
- package/dist/doctor/index.d.ts +1 -0
- package/dist/doctor/index.js +1 -0
- package/dist/doctor/main.js +22 -0
- package/dist/doctor/openclaw-skills.d.ts +143 -0
- package/dist/doctor/openclaw-skills.js +706 -0
- package/dist/doctor/types.d.ts +11 -0
- package/dist/eval/health.d.ts +10 -1
- package/dist/eval/health.js +16 -16
- package/dist/hooks/dispatcher.js +20 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init-deposit/hygiene.js +3 -0
- package/dist/init-deposit/init-deposit.d.ts +5 -0
- package/dist/init-deposit/init-deposit.js +37 -2
- package/dist/intake/github-auth-modes.d.ts +1 -1
- package/dist/intake/github-auth-modes.js +25 -3
- package/dist/intake/issue-ingest-cli.js +13 -0
- package/dist/intake/reconcile-issues-cli.js +13 -0
- package/dist/lifecycle/events.js +2 -0
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.js +1 -0
- package/dist/lifecycle/stats.d.ts +53 -0
- package/dist/lifecycle/stats.js +286 -0
- package/dist/policy/plan-extensions.d.ts +10 -0
- package/dist/policy/plan-extensions.js +16 -0
- package/dist/pr-closing-keywords/detect.d.ts +13 -0
- package/dist/pr-closing-keywords/detect.js +73 -8
- package/dist/pr-closing-keywords/index.d.ts +2 -2
- package/dist/pr-closing-keywords/index.js +1 -1
- package/dist/pr-closing-keywords/main.js +119 -90
- package/dist/pr-closing-keywords/types.d.ts +10 -0
- package/dist/release/spawn.js +13 -0
- package/dist/release-e2e/git-ops.d.ts +7 -0
- package/dist/release-e2e/git-ops.js +35 -2
- package/dist/render/constants.d.ts +16 -0
- package/dist/render/constants.js +16 -1
- package/dist/render/export-spec.d.ts +7 -1
- package/dist/render/export-spec.js +51 -5
- package/dist/render/index.d.ts +3 -2
- package/dist/render/index.js +2 -2
- package/dist/render/roadmap-render.js +164 -60
- package/dist/render/scope-outlook.d.ts +5 -0
- package/dist/render/scope-outlook.js +3 -0
- package/dist/render/spec-render.d.ts +28 -2
- package/dist/render/spec-render.js +111 -14
- package/dist/render/text-utils.d.ts +6 -0
- package/dist/render/text-utils.js +23 -0
- package/dist/scm/binary.d.ts +3 -0
- package/dist/scm/binary.js +10 -2
- package/dist/scm/call.d.ts +5 -0
- package/dist/scm/call.js +5 -0
- package/dist/scm/index.d.ts +1 -0
- package/dist/scm/index.js +1 -0
- package/dist/scm/main.d.ts +5 -0
- package/dist/scm/main.js +27 -0
- package/dist/scm/readiness-cli.d.ts +24 -0
- package/dist/scm/readiness-cli.js +85 -0
- package/dist/scm/readiness.d.ts +104 -0
- package/dist/scm/readiness.js +421 -0
- package/dist/scope/batch-promote.d.ts +26 -0
- package/dist/scope/batch-promote.js +167 -0
- package/dist/scope/index.d.ts +1 -0
- package/dist/scope/index.js +1 -0
- package/dist/scope/main.d.ts +2 -0
- package/dist/scope/main.js +58 -7
- package/dist/scope/wip-cap-check.d.ts +6 -0
- package/dist/scope/wip-cap-check.js +14 -2
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +2 -0
- package/dist/session/process-cost-constants.d.ts +9 -0
- package/dist/session/process-cost-constants.js +11 -0
- package/dist/session/process-cost.d.ts +53 -0
- package/dist/session/process-cost.js +82 -0
- package/dist/session/ritual-sentinel.d.ts +5 -0
- package/dist/session/ritual-sentinel.js +12 -1
- package/dist/session/session-ready.d.ts +67 -0
- package/dist/session/session-ready.js +264 -0
- package/dist/session/session-start.d.ts +63 -1
- package/dist/session/session-start.js +432 -24
- package/dist/session/verify-session-ritual.d.ts +8 -0
- package/dist/session/verify-session-ritual.js +95 -35
- package/dist/tool-events/classify.d.ts +20 -0
- package/dist/tool-events/classify.js +634 -0
- package/dist/tool-events/index.d.ts +10 -0
- package/dist/tool-events/index.js +10 -0
- package/dist/tool-events/summarize.d.ts +34 -0
- package/dist/tool-events/summarize.js +93 -0
- package/dist/tool-events/types.d.ts +52 -0
- package/dist/tool-events/types.js +13 -0
- package/dist/triage/welcome/index.d.ts +1 -1
- package/dist/triage/welcome/index.js +1 -1
- package/dist/triage/welcome/onboard.js +7 -1
- package/dist/triage/welcome/prior-state.d.ts +7 -0
- package/dist/triage/welcome/prior-state.js +16 -2
- package/dist/triage/welcome/writers.d.ts +10 -0
- package/dist/triage/welcome/writers.js +72 -2
- 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
|