token-pilot 0.30.4 → 0.31.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/agents/tp-api-surface-tracker.md +10 -2
- package/agents/tp-audit-scanner.md +10 -2
- package/agents/tp-commit-writer.md +10 -2
- package/agents/tp-context-engineer.md +10 -2
- package/agents/tp-dead-code-finder.md +10 -2
- package/agents/tp-debugger.md +10 -2
- package/agents/tp-dep-health.md +10 -2
- package/agents/tp-doc-writer.md +10 -2
- package/agents/tp-history-explorer.md +10 -2
- package/agents/tp-impact-analyzer.md +10 -2
- package/agents/tp-incident-timeline.md +10 -2
- package/agents/tp-incremental-builder.md +10 -2
- package/agents/tp-migration-scout.md +10 -2
- package/agents/tp-onboard.md +10 -2
- package/agents/tp-performance-profiler.md +10 -2
- package/agents/tp-pr-reviewer.md +10 -2
- package/agents/tp-refactor-planner.md +10 -2
- package/agents/tp-review-impact.md +10 -2
- package/agents/tp-run.md +10 -2
- package/agents/tp-session-restorer.md +10 -2
- package/agents/tp-ship-coordinator.md +10 -2
- package/agents/tp-spec-writer.md +10 -2
- package/agents/tp-test-coverage-gapper.md +10 -2
- package/agents/tp-test-triage.md +10 -2
- package/agents/tp-test-writer.md +10 -2
- package/dist/cli/stats.d.ts +2 -0
- package/dist/cli/stats.js +46 -1
- package/dist/core/agent-matcher.d.ts +115 -0
- package/dist/core/agent-matcher.js +326 -0
- package/dist/core/event-log.d.ts +14 -1
- package/dist/hooks/installer.js +9 -0
- package/dist/hooks/post-task.d.ts +15 -0
- package/dist/hooks/post-task.js +102 -19
- package/dist/hooks/pre-bash.js +10 -2
- package/dist/hooks/pre-task.d.ts +71 -0
- package/dist/hooks/pre-task.js +125 -0
- package/dist/index.js +29 -0
- package/hooks/hooks.json +9 -0
- package/package.json +1 -1
package/dist/cli/stats.js
CHANGED
|
@@ -67,7 +67,50 @@ export function formatStats(events, opts) {
|
|
|
67
67
|
const total = sumSaved(scope);
|
|
68
68
|
const sessionSuffix = sessionLabel ? ` (session ${sessionLabel})` : "";
|
|
69
69
|
lines.push(`token-pilot stats${sessionSuffix} — ${scope.length} event${scope.length === 1 ? "" : "s"}, ~${total} tokens saved`);
|
|
70
|
-
if (opts.
|
|
70
|
+
if (opts.tasks) {
|
|
71
|
+
// v0.31.0 — Task-routing view. Scope to event:"task" records only.
|
|
72
|
+
const taskEvents = scope.filter((e) => e.event === "task");
|
|
73
|
+
if (taskEvents.length === 0) {
|
|
74
|
+
return lines[0] + "\n\nNo Task events yet.";
|
|
75
|
+
}
|
|
76
|
+
const totalTasks = taskEvents.length;
|
|
77
|
+
const misses = taskEvents.filter((e) => typeof e.matched_tp_agent === "string" &&
|
|
78
|
+
e.matched_tp_agent.length > 0 &&
|
|
79
|
+
e.subagent_type !== e.matched_tp_agent);
|
|
80
|
+
const missRate = totalTasks > 0 ? Math.round((misses.length / totalTasks) * 100) : 0;
|
|
81
|
+
// Group by subagent_type (what Claude actually picked).
|
|
82
|
+
const pickGroups = groupBy(taskEvents, (e) => (e.subagent_type && e.subagent_type.length > 0
|
|
83
|
+
? e.subagent_type
|
|
84
|
+
: "(unknown)"));
|
|
85
|
+
const picks = [...pickGroups.entries()]
|
|
86
|
+
.map(([agent, evs]) => ({ agent, count: evs.length }))
|
|
87
|
+
.sort((a, b) => b.count - a.count);
|
|
88
|
+
// Top missed routings: (picked → suggested) pairs with counts.
|
|
89
|
+
const missCounts = new Map();
|
|
90
|
+
for (const e of misses) {
|
|
91
|
+
const key = `${e.subagent_type} → ${e.matched_tp_agent}`;
|
|
92
|
+
missCounts.set(key, (missCounts.get(key) ?? 0) + 1);
|
|
93
|
+
}
|
|
94
|
+
const topMisses = [...missCounts.entries()]
|
|
95
|
+
.map(([pair, count]) => ({ pair, count }))
|
|
96
|
+
.sort((a, b) => b.count - a.count)
|
|
97
|
+
.slice(0, 10);
|
|
98
|
+
// Rewrite header for the task view (replace savings number with miss-rate).
|
|
99
|
+
lines[0] = `token-pilot stats — ${totalTasks} Task call${totalTasks === 1 ? "" : "s"}, miss-rate ${missRate}% (${misses.length}/${totalTasks})${sessionSuffix}`;
|
|
100
|
+
lines.push("");
|
|
101
|
+
lines.push("Picked subagents:");
|
|
102
|
+
for (const p of picks) {
|
|
103
|
+
lines.push(` ${pad(p.agent, 24)} ${p.count.toString().padStart(4)}× events`);
|
|
104
|
+
}
|
|
105
|
+
if (topMisses.length > 0) {
|
|
106
|
+
lines.push("");
|
|
107
|
+
lines.push("Top routing misses (picked → suggested tp-*):");
|
|
108
|
+
for (const m of topMisses) {
|
|
109
|
+
lines.push(` ${pad(m.pair, 48)} ${m.count.toString().padStart(4)}×`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else if (opts.byAgent) {
|
|
71
114
|
// Group by agent_type (null → "main").
|
|
72
115
|
const groups = groupBy(scope, (e) => (e.agent_type ?? "main"));
|
|
73
116
|
const rows = [...groups.entries()]
|
|
@@ -121,9 +164,11 @@ export async function handleStats(argv, opts) {
|
|
|
121
164
|
const events = await loadEvents(projectRoot);
|
|
122
165
|
const session = parseFlag(argv, "session");
|
|
123
166
|
const byAgent = parseFlag(argv, "by-agent");
|
|
167
|
+
const tasks = parseFlag(argv, "tasks");
|
|
124
168
|
const rendered = formatStats(events, {
|
|
125
169
|
session: session === undefined ? undefined : session,
|
|
126
170
|
byAgent: byAgent === true,
|
|
171
|
+
tasks: tasks === true,
|
|
127
172
|
});
|
|
128
173
|
process.stdout.write(rendered + "\n");
|
|
129
174
|
return 0;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.31.0 — tp-* subagent heuristic matcher.
|
|
3
|
+
*
|
|
4
|
+
* Goal: given a Claude Code `Task` tool invocation (subagent_type +
|
|
5
|
+
* description), decide which `tp-*` agent from `agents/` would be a
|
|
6
|
+
* better fit. Used by:
|
|
7
|
+
*
|
|
8
|
+
* 1. PostToolUse:Task telemetry — enrich each event with
|
|
9
|
+
* `matched_tp_agent` so `stats --tasks` can show miss-rate.
|
|
10
|
+
* 2. PreToolUse:Task enforcement (Pack 2, later) — advise/deny when
|
|
11
|
+
* the agent picked `general-purpose` but a tp-* clearly fits.
|
|
12
|
+
*
|
|
13
|
+
* Matcher philosophy — keep it BORING and EXPLAINABLE.
|
|
14
|
+
*
|
|
15
|
+
* Agent frontmatter description layout (empirically stable across all
|
|
16
|
+
* 24 shipped agents):
|
|
17
|
+
*
|
|
18
|
+
* description: PROACTIVELY use this when the user asks to review a
|
|
19
|
+
* diff, PR, commit range, or changeset ("review these changes",
|
|
20
|
+
* "look at my PR", "is this safe to merge"). Verdict-first output
|
|
21
|
+
* with Critical / Important findings. Do NOT use for writing code
|
|
22
|
+
* or planning.
|
|
23
|
+
*
|
|
24
|
+
* Two signal sources:
|
|
25
|
+
*
|
|
26
|
+
* - Quoted triggers: every `"…"` substring inside the description.
|
|
27
|
+
* These are literally the phrases the agent author expected users
|
|
28
|
+
* to type. Highest signal. Substring match (case-insensitive) on
|
|
29
|
+
* the user's description → score += 2.
|
|
30
|
+
*
|
|
31
|
+
* - Content keywords: stemmed word set from the 1st description
|
|
32
|
+
* sentence, minus stopwords and boilerplate ("PROACTIVELY",
|
|
33
|
+
* "use this", "when the user asks"). Each match on the user's
|
|
34
|
+
* description → score += 1.
|
|
35
|
+
*
|
|
36
|
+
* Negative filter: everything after `Do NOT use for` is excluded from
|
|
37
|
+
* keyword extraction AND actively penalises a match (score -= 1 per
|
|
38
|
+
* term present in user's description). Prevents `tp-test-writer` from
|
|
39
|
+
* being suggested on "diagnose failing test" (which is tp-test-triage).
|
|
40
|
+
*
|
|
41
|
+
* Confidence tiers:
|
|
42
|
+
* - score ≥ 3 or ≥ 1 quoted trigger → "high"
|
|
43
|
+
* - score in [1, 2] → "low"
|
|
44
|
+
* - score < 1 → no match
|
|
45
|
+
*
|
|
46
|
+
* The function is pure (deps → in-memory index + string) so it's fully
|
|
47
|
+
* unit-testable. File I/O (reading the agents dir) lives in
|
|
48
|
+
* `buildAgentIndex` which is a one-shot loader called at startup.
|
|
49
|
+
*/
|
|
50
|
+
/** One parsed `tp-*` agent. Only fields the matcher needs. */
|
|
51
|
+
export interface ParsedAgent {
|
|
52
|
+
/** agent name without .md extension, e.g. "tp-pr-reviewer" */
|
|
53
|
+
name: string;
|
|
54
|
+
/** phrases found in `"…"` inside the description — highest signal */
|
|
55
|
+
quotedTriggers: string[];
|
|
56
|
+
/** stemmed content keywords from the positive side of the description */
|
|
57
|
+
keywords: string[];
|
|
58
|
+
/** negative-filter terms from `Do NOT use for …` */
|
|
59
|
+
negative: string[];
|
|
60
|
+
}
|
|
61
|
+
export interface AgentIndex {
|
|
62
|
+
agents: ParsedAgent[];
|
|
63
|
+
}
|
|
64
|
+
export interface MatchResult {
|
|
65
|
+
agent: string;
|
|
66
|
+
confidence: "high" | "low";
|
|
67
|
+
score: number;
|
|
68
|
+
}
|
|
69
|
+
/** Extract the `description:` value from YAML frontmatter.
|
|
70
|
+
* Supports multi-line values (continuation lines indented).
|
|
71
|
+
* Returns null when the file has no frontmatter or no description. */
|
|
72
|
+
export declare function extractDescription(body: string): string | null;
|
|
73
|
+
/** Pull every `"…"` substring out of a string. Ignores empty pairs. */
|
|
74
|
+
export declare function extractQuotedTriggers(s: string): string[];
|
|
75
|
+
/**
|
|
76
|
+
* Split `description` around `Do NOT use for …` — everything on the
|
|
77
|
+
* positive side is keyword material; everything after contributes to
|
|
78
|
+
* the negative filter.
|
|
79
|
+
*/
|
|
80
|
+
export declare function splitAroundNegative(desc: string): {
|
|
81
|
+
positive: string;
|
|
82
|
+
negative: string;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Tokenise → lowercase → drop stopwords + ≤2 chars + quoted-trigger
|
|
86
|
+
* leftovers. Keywords stay in surface form; we do not stem. Stemming
|
|
87
|
+
* helps recall on English verb/noun pairs ("refactor"/"refactoring"),
|
|
88
|
+
* but libraries add cost for modest gain — use substring match on the
|
|
89
|
+
* user's description instead (covers most morphology).
|
|
90
|
+
*/
|
|
91
|
+
export declare function extractKeywords(text: string): string[];
|
|
92
|
+
/**
|
|
93
|
+
* Parse one agent markdown body into its ParsedAgent representation.
|
|
94
|
+
* Returns null if frontmatter is missing / description is empty.
|
|
95
|
+
*/
|
|
96
|
+
export declare function parseAgent(name: string, body: string): ParsedAgent | null;
|
|
97
|
+
/**
|
|
98
|
+
* Load every `tp-*.md` under a directory and build an in-memory index.
|
|
99
|
+
* Non-tp-* files are silently skipped. Unreadable files are skipped
|
|
100
|
+
* with no throw — an agent directory isn't a runtime dep.
|
|
101
|
+
*/
|
|
102
|
+
export declare function buildAgentIndex(agentsDir: string): Promise<AgentIndex>;
|
|
103
|
+
/**
|
|
104
|
+
* Score a single agent against the user description. Surface the score
|
|
105
|
+
* so callers can inspect / threshold differently if needed.
|
|
106
|
+
*/
|
|
107
|
+
export declare function scoreAgent(agent: ParsedAgent, userDescriptionLower: string): number;
|
|
108
|
+
/**
|
|
109
|
+
* Find the best `tp-*` match for a user description. Returns null when
|
|
110
|
+
* no agent clears the low-confidence threshold.
|
|
111
|
+
*
|
|
112
|
+
* "Best" = highest score, tiebreak alphabetical (deterministic).
|
|
113
|
+
*/
|
|
114
|
+
export declare function matchTpAgent(description: string, index: AgentIndex): MatchResult | null;
|
|
115
|
+
//# sourceMappingURL=agent-matcher.d.ts.map
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.31.0 — tp-* subagent heuristic matcher.
|
|
3
|
+
*
|
|
4
|
+
* Goal: given a Claude Code `Task` tool invocation (subagent_type +
|
|
5
|
+
* description), decide which `tp-*` agent from `agents/` would be a
|
|
6
|
+
* better fit. Used by:
|
|
7
|
+
*
|
|
8
|
+
* 1. PostToolUse:Task telemetry — enrich each event with
|
|
9
|
+
* `matched_tp_agent` so `stats --tasks` can show miss-rate.
|
|
10
|
+
* 2. PreToolUse:Task enforcement (Pack 2, later) — advise/deny when
|
|
11
|
+
* the agent picked `general-purpose` but a tp-* clearly fits.
|
|
12
|
+
*
|
|
13
|
+
* Matcher philosophy — keep it BORING and EXPLAINABLE.
|
|
14
|
+
*
|
|
15
|
+
* Agent frontmatter description layout (empirically stable across all
|
|
16
|
+
* 24 shipped agents):
|
|
17
|
+
*
|
|
18
|
+
* description: PROACTIVELY use this when the user asks to review a
|
|
19
|
+
* diff, PR, commit range, or changeset ("review these changes",
|
|
20
|
+
* "look at my PR", "is this safe to merge"). Verdict-first output
|
|
21
|
+
* with Critical / Important findings. Do NOT use for writing code
|
|
22
|
+
* or planning.
|
|
23
|
+
*
|
|
24
|
+
* Two signal sources:
|
|
25
|
+
*
|
|
26
|
+
* - Quoted triggers: every `"…"` substring inside the description.
|
|
27
|
+
* These are literally the phrases the agent author expected users
|
|
28
|
+
* to type. Highest signal. Substring match (case-insensitive) on
|
|
29
|
+
* the user's description → score += 2.
|
|
30
|
+
*
|
|
31
|
+
* - Content keywords: stemmed word set from the 1st description
|
|
32
|
+
* sentence, minus stopwords and boilerplate ("PROACTIVELY",
|
|
33
|
+
* "use this", "when the user asks"). Each match on the user's
|
|
34
|
+
* description → score += 1.
|
|
35
|
+
*
|
|
36
|
+
* Negative filter: everything after `Do NOT use for` is excluded from
|
|
37
|
+
* keyword extraction AND actively penalises a match (score -= 1 per
|
|
38
|
+
* term present in user's description). Prevents `tp-test-writer` from
|
|
39
|
+
* being suggested on "diagnose failing test" (which is tp-test-triage).
|
|
40
|
+
*
|
|
41
|
+
* Confidence tiers:
|
|
42
|
+
* - score ≥ 3 or ≥ 1 quoted trigger → "high"
|
|
43
|
+
* - score in [1, 2] → "low"
|
|
44
|
+
* - score < 1 → no match
|
|
45
|
+
*
|
|
46
|
+
* The function is pure (deps → in-memory index + string) so it's fully
|
|
47
|
+
* unit-testable. File I/O (reading the agents dir) lives in
|
|
48
|
+
* `buildAgentIndex` which is a one-shot loader called at startup.
|
|
49
|
+
*/
|
|
50
|
+
import { promises as fs } from "node:fs";
|
|
51
|
+
import { join } from "node:path";
|
|
52
|
+
/**
|
|
53
|
+
* Stopwords stripped from keyword extraction. Keep tiny — aggressive
|
|
54
|
+
* stopword lists kill recall. Only boilerplate from agent frontmatter
|
|
55
|
+
* templates goes here.
|
|
56
|
+
*/
|
|
57
|
+
const STOPWORDS = new Set([
|
|
58
|
+
"a",
|
|
59
|
+
"an",
|
|
60
|
+
"the",
|
|
61
|
+
"and",
|
|
62
|
+
"or",
|
|
63
|
+
"of",
|
|
64
|
+
"to",
|
|
65
|
+
"in",
|
|
66
|
+
"for",
|
|
67
|
+
"on",
|
|
68
|
+
"at",
|
|
69
|
+
"by",
|
|
70
|
+
"is",
|
|
71
|
+
"are",
|
|
72
|
+
"was",
|
|
73
|
+
"were",
|
|
74
|
+
"be",
|
|
75
|
+
"been",
|
|
76
|
+
"being",
|
|
77
|
+
"this",
|
|
78
|
+
"that",
|
|
79
|
+
"these",
|
|
80
|
+
"those",
|
|
81
|
+
"it",
|
|
82
|
+
"its",
|
|
83
|
+
"as",
|
|
84
|
+
"with",
|
|
85
|
+
"when",
|
|
86
|
+
"where",
|
|
87
|
+
"user",
|
|
88
|
+
"users",
|
|
89
|
+
"ask",
|
|
90
|
+
"asks",
|
|
91
|
+
"asked",
|
|
92
|
+
"asking",
|
|
93
|
+
"use",
|
|
94
|
+
"uses",
|
|
95
|
+
"used",
|
|
96
|
+
"using",
|
|
97
|
+
"proactively",
|
|
98
|
+
"please",
|
|
99
|
+
"any",
|
|
100
|
+
"all",
|
|
101
|
+
"some",
|
|
102
|
+
"get",
|
|
103
|
+
"gets",
|
|
104
|
+
"got",
|
|
105
|
+
"also",
|
|
106
|
+
"like",
|
|
107
|
+
"from",
|
|
108
|
+
"into",
|
|
109
|
+
"not",
|
|
110
|
+
"no",
|
|
111
|
+
"do",
|
|
112
|
+
"does",
|
|
113
|
+
"did",
|
|
114
|
+
"have",
|
|
115
|
+
"has",
|
|
116
|
+
"had",
|
|
117
|
+
"will",
|
|
118
|
+
"can",
|
|
119
|
+
"could",
|
|
120
|
+
"should",
|
|
121
|
+
"may",
|
|
122
|
+
"might",
|
|
123
|
+
"must",
|
|
124
|
+
"you",
|
|
125
|
+
"your",
|
|
126
|
+
"their",
|
|
127
|
+
"they",
|
|
128
|
+
"them",
|
|
129
|
+
"we",
|
|
130
|
+
"our",
|
|
131
|
+
"us",
|
|
132
|
+
]);
|
|
133
|
+
/** Extract the `description:` value from YAML frontmatter.
|
|
134
|
+
* Supports multi-line values (continuation lines indented).
|
|
135
|
+
* Returns null when the file has no frontmatter or no description. */
|
|
136
|
+
export function extractDescription(body) {
|
|
137
|
+
const fmEnd = body.indexOf("\n---", 3);
|
|
138
|
+
if (!body.startsWith("---\n") || fmEnd === -1)
|
|
139
|
+
return null;
|
|
140
|
+
const fm = body.slice(4, fmEnd);
|
|
141
|
+
const lines = fm.split("\n");
|
|
142
|
+
let desc = "";
|
|
143
|
+
let inDesc = false;
|
|
144
|
+
for (const line of lines) {
|
|
145
|
+
// Top-level key detection: `key: value` at column 0
|
|
146
|
+
const topKey = /^([A-Za-z_][\w-]*)\s*:\s*(.*)$/.exec(line);
|
|
147
|
+
if (topKey && !/^\s/.test(line)) {
|
|
148
|
+
if (inDesc)
|
|
149
|
+
break;
|
|
150
|
+
if (topKey[1] === "description") {
|
|
151
|
+
desc = topKey[2] ?? "";
|
|
152
|
+
inDesc = true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else if (inDesc) {
|
|
156
|
+
// Continuation line (indented or blank) — append with a space.
|
|
157
|
+
desc += " " + line.trim();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const trimmed = desc.trim();
|
|
161
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
162
|
+
}
|
|
163
|
+
/** Pull every `"…"` substring out of a string. Ignores empty pairs. */
|
|
164
|
+
export function extractQuotedTriggers(s) {
|
|
165
|
+
const out = [];
|
|
166
|
+
const re = /"([^"]+)"/g;
|
|
167
|
+
for (const m of s.matchAll(re)) {
|
|
168
|
+
const inner = m[1].trim().toLowerCase();
|
|
169
|
+
if (inner.length > 0)
|
|
170
|
+
out.push(inner);
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Split `description` around `Do NOT use for …` — everything on the
|
|
176
|
+
* positive side is keyword material; everything after contributes to
|
|
177
|
+
* the negative filter.
|
|
178
|
+
*/
|
|
179
|
+
export function splitAroundNegative(desc) {
|
|
180
|
+
// Case-insensitive split on common "Do NOT use …" lead-ins. `to` catches
|
|
181
|
+
// "Do NOT use to write" (tp-test-triage); `for` / `on` / `during` / `when`
|
|
182
|
+
// cover every other shipped agent. Add terms here as new forms appear.
|
|
183
|
+
const re = /\bdo\s+not\s+use\s+(?:for|on|during|when|to)\b/i;
|
|
184
|
+
const idx = desc.search(re);
|
|
185
|
+
if (idx === -1)
|
|
186
|
+
return { positive: desc, negative: "" };
|
|
187
|
+
return {
|
|
188
|
+
positive: desc.slice(0, idx),
|
|
189
|
+
negative: desc.slice(idx),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Tokenise → lowercase → drop stopwords + ≤2 chars + quoted-trigger
|
|
194
|
+
* leftovers. Keywords stay in surface form; we do not stem. Stemming
|
|
195
|
+
* helps recall on English verb/noun pairs ("refactor"/"refactoring"),
|
|
196
|
+
* but libraries add cost for modest gain — use substring match on the
|
|
197
|
+
* user's description instead (covers most morphology).
|
|
198
|
+
*/
|
|
199
|
+
export function extractKeywords(text) {
|
|
200
|
+
const out = new Set();
|
|
201
|
+
// Remove quoted phrases first (they're handled separately).
|
|
202
|
+
const cleaned = text.replace(/"[^"]+"/g, " ");
|
|
203
|
+
for (const raw of cleaned.toLowerCase().split(/[^a-z0-9_-]+/)) {
|
|
204
|
+
const tok = raw.trim();
|
|
205
|
+
// Keep short technical terms ("ci", "pr", "db", "io"). STOPWORDS already
|
|
206
|
+
// filters most 1-2 char english junk ("is", "to", "on", "a"). Drop
|
|
207
|
+
// single chars only — they carry ~no signal.
|
|
208
|
+
if (tok.length < 2)
|
|
209
|
+
continue;
|
|
210
|
+
if (STOPWORDS.has(tok))
|
|
211
|
+
continue;
|
|
212
|
+
out.add(tok);
|
|
213
|
+
}
|
|
214
|
+
return [...out];
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Parse one agent markdown body into its ParsedAgent representation.
|
|
218
|
+
* Returns null if frontmatter is missing / description is empty.
|
|
219
|
+
*/
|
|
220
|
+
export function parseAgent(name, body) {
|
|
221
|
+
const desc = extractDescription(body);
|
|
222
|
+
if (!desc)
|
|
223
|
+
return null;
|
|
224
|
+
const { positive, negative } = splitAroundNegative(desc);
|
|
225
|
+
const quotedTriggers = extractQuotedTriggers(desc);
|
|
226
|
+
const keywords = extractKeywords(positive);
|
|
227
|
+
// Negative terms: only the core ones (tp-* names, salient nouns).
|
|
228
|
+
const negKeywords = extractKeywords(negative);
|
|
229
|
+
return {
|
|
230
|
+
name,
|
|
231
|
+
quotedTriggers,
|
|
232
|
+
keywords,
|
|
233
|
+
negative: negKeywords,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Load every `tp-*.md` under a directory and build an in-memory index.
|
|
238
|
+
* Non-tp-* files are silently skipped. Unreadable files are skipped
|
|
239
|
+
* with no throw — an agent directory isn't a runtime dep.
|
|
240
|
+
*/
|
|
241
|
+
export async function buildAgentIndex(agentsDir) {
|
|
242
|
+
let entries;
|
|
243
|
+
try {
|
|
244
|
+
entries = await fs.readdir(agentsDir);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return { agents: [] };
|
|
248
|
+
}
|
|
249
|
+
const agents = [];
|
|
250
|
+
for (const entry of entries) {
|
|
251
|
+
if (!entry.startsWith("tp-") || !entry.endsWith(".md"))
|
|
252
|
+
continue;
|
|
253
|
+
const name = entry.slice(0, -".md".length);
|
|
254
|
+
let body;
|
|
255
|
+
try {
|
|
256
|
+
body = await fs.readFile(join(agentsDir, entry), "utf-8");
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const parsed = parseAgent(name, body);
|
|
262
|
+
if (parsed)
|
|
263
|
+
agents.push(parsed);
|
|
264
|
+
}
|
|
265
|
+
return { agents };
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Score a single agent against the user description. Surface the score
|
|
269
|
+
* so callers can inspect / threshold differently if needed.
|
|
270
|
+
*/
|
|
271
|
+
export function scoreAgent(agent, userDescriptionLower) {
|
|
272
|
+
let score = 0;
|
|
273
|
+
for (const trigger of agent.quotedTriggers) {
|
|
274
|
+
if (userDescriptionLower.includes(trigger))
|
|
275
|
+
score += 2;
|
|
276
|
+
}
|
|
277
|
+
for (const kw of agent.keywords) {
|
|
278
|
+
if (userDescriptionLower.includes(kw))
|
|
279
|
+
score += 1;
|
|
280
|
+
}
|
|
281
|
+
for (const neg of agent.negative) {
|
|
282
|
+
if (userDescriptionLower.includes(neg))
|
|
283
|
+
score -= 1;
|
|
284
|
+
}
|
|
285
|
+
return score;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Find the best `tp-*` match for a user description. Returns null when
|
|
289
|
+
* no agent clears the low-confidence threshold.
|
|
290
|
+
*
|
|
291
|
+
* "Best" = highest score, tiebreak alphabetical (deterministic).
|
|
292
|
+
*/
|
|
293
|
+
export function matchTpAgent(description, index) {
|
|
294
|
+
if (!description || index.agents.length === 0)
|
|
295
|
+
return null;
|
|
296
|
+
const needle = description.toLowerCase();
|
|
297
|
+
let best = null;
|
|
298
|
+
for (const agent of index.agents) {
|
|
299
|
+
const score = scoreAgent(agent, needle);
|
|
300
|
+
if (!best) {
|
|
301
|
+
best = { agent, score };
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (score > best.score) {
|
|
305
|
+
best = { agent, score };
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
// Deterministic tiebreak: alphabetical by agent.name. Without this,
|
|
309
|
+
// match depends on readdir order, which is filesystem-specific.
|
|
310
|
+
if (score === best.score && agent.name < best.agent.name) {
|
|
311
|
+
best = { agent, score };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (!best || best.score < 1)
|
|
315
|
+
return null;
|
|
316
|
+
// High confidence when score is strong OR at least one quoted trigger
|
|
317
|
+
// matched (quoted = explicit author-blessed phrase).
|
|
318
|
+
const hitQuoted = best.agent.quotedTriggers.some((t) => needle.includes(t));
|
|
319
|
+
const confidence = best.score >= 3 || hitQuoted ? "high" : "low";
|
|
320
|
+
return {
|
|
321
|
+
agent: best.agent.name,
|
|
322
|
+
confidence,
|
|
323
|
+
score: best.score,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
//# sourceMappingURL=agent-matcher.js.map
|
package/dist/core/event-log.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export interface HookEvent {
|
|
|
28
28
|
/** null for top-level session; agent_type string inside a subagent. */
|
|
29
29
|
agent_type: string | null;
|
|
30
30
|
agent_id: string | null;
|
|
31
|
-
event: "denied" | "allowed" | "bypass" | "pass-through" | string;
|
|
31
|
+
event: "denied" | "allowed" | "bypass" | "pass-through" | "task" | string;
|
|
32
32
|
file: string;
|
|
33
33
|
lines: number;
|
|
34
34
|
estTokens: number;
|
|
@@ -36,6 +36,19 @@ export interface HookEvent {
|
|
|
36
36
|
summaryTokens: number;
|
|
37
37
|
/** estTokens - summaryTokens; 0 for allow/bypass. */
|
|
38
38
|
savedTokens: number;
|
|
39
|
+
/** The subagent_type Claude Code dispatched (`tp-*` or `general-purpose`…). */
|
|
40
|
+
subagent_type?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Heuristic match against `tp-*` agent frontmatter. Set only when
|
|
43
|
+
* `subagent_type` is NOT already a tp-*. null when no match fires.
|
|
44
|
+
*/
|
|
45
|
+
matched_tp_agent?: string | null;
|
|
46
|
+
/** Confidence of the heuristic match (omitted when matched_tp_agent=null). */
|
|
47
|
+
match_confidence?: "high" | "low";
|
|
48
|
+
/** Response budget declared in the agent markdown body, or null. */
|
|
49
|
+
budget?: number | null;
|
|
50
|
+
/** actualTokens > budget × (1 + tolerance). */
|
|
51
|
+
overBudget?: boolean;
|
|
39
52
|
}
|
|
40
53
|
export declare function eventLogDir(projectRoot: string): string;
|
|
41
54
|
export declare function currentLogPath(projectRoot: string): string;
|
package/dist/hooks/installer.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* Silent on every failure — telemetry must never break the agent loop.
|
|
14
14
|
* Non-tp-* subagents are ignored (we only enforce our own contracts).
|
|
15
15
|
*/
|
|
16
|
+
import { type AgentIndex } from "../core/agent-matcher.js";
|
|
16
17
|
export declare const OVER_BUDGET_LOG = "over-budget.log";
|
|
17
18
|
/** Ratio above which we flag — 0.1 = 10 % grace. */
|
|
18
19
|
export declare const OVER_BUDGET_TOLERANCE = 0.1;
|
|
@@ -55,9 +56,23 @@ export interface PostTaskHookInput {
|
|
|
55
56
|
tool_name?: string;
|
|
56
57
|
tool_input?: {
|
|
57
58
|
subagent_type?: string;
|
|
59
|
+
description?: string;
|
|
58
60
|
};
|
|
59
61
|
tool_response?: unknown;
|
|
62
|
+
session_id?: string;
|
|
63
|
+
agent_type?: string;
|
|
64
|
+
agent_id?: string;
|
|
60
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the plugin's own `agents/` directory. The hook binary lives
|
|
68
|
+
* at `<plugin>/dist/index.js`, so agents/ is `../agents` from here.
|
|
69
|
+
* Allow an override for tests that want an isolated fixture dir.
|
|
70
|
+
*/
|
|
71
|
+
export declare function defaultAgentsDir(): string;
|
|
72
|
+
/** Resolve (and cache) the tp-* agent index. Safe to call repeatedly. */
|
|
73
|
+
export declare function getAgentIndex(dir?: string): Promise<AgentIndex>;
|
|
74
|
+
/** Test-only: clear the module-level cache between fixtures. */
|
|
75
|
+
export declare function _resetAgentIndexCache(): void;
|
|
61
76
|
/**
|
|
62
77
|
* Full post-Task processing: read frontmatter, count tokens, log over-budget.
|
|
63
78
|
* Returns the advice message (or null) so the caller can optionally emit
|