codehere 0.3.0-rc.6 → 0.3.0-rc.7

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.
@@ -65,36 +65,94 @@ const ROUTING_RULES = [
65
65
  ];
66
66
  /** Default specialist when no routing rule matches. */
67
67
  const DEFAULT_SPECIALIST = 'claude-code';
68
+ /**
69
+ * Tokenize a prompt for trigger matching. Splits on any non-alphanumeric
70
+ * boundary (whitespace, hyphens, punctuation), lowercases everything. So:
71
+ *
72
+ * "Bug fix" → ["bug", "fix"]
73
+ * "set-up auth" → ["set", "up", "auth"] (trigger "set up" still matches via phrase below)
74
+ * "unit-test it" → ["unit", "test", "it"]
75
+ * "prefix matter" → ["prefix", "matter"] (does NOT match `fix`)
76
+ * "hotfixes" → ["hotfixes"] (does NOT match `fix`)
77
+ *
78
+ * Multi-word triggers ("add tests", "create new") are matched against the
79
+ * normalized prompt directly using word boundaries — so they catch
80
+ * "add tests" but not "added testing harness".
81
+ */
82
+ function tokenize(prompt) {
83
+ const tokens = new Set(prompt
84
+ .toLowerCase()
85
+ .split(/[^a-z0-9]+/)
86
+ .filter(Boolean));
87
+ // Normalized form for multi-word phrase matching: hyphens/underscores → space,
88
+ // collapse runs of whitespace. Leaves alphanumerics intact.
89
+ const normalized = ' ' + prompt.toLowerCase().replace(/[-_]+/g, ' ').replace(/\s+/g, ' ').trim() + ' ';
90
+ return { tokens, normalized };
91
+ }
92
+ /**
93
+ * Test whether a routing trigger matches a prompt. Single-word triggers must
94
+ * appear as whole tokens (no substring matches). Multi-word triggers must
95
+ * appear as a phrase in the normalized prompt (with leading/trailing spaces
96
+ * to enforce word boundaries on the ends).
97
+ */
98
+ function triggerMatches(trigger, tokens, normalized) {
99
+ const t = trigger.toLowerCase().trim();
100
+ if (!t)
101
+ return false;
102
+ if (t.includes(' ')) {
103
+ return normalized.includes(' ' + t + ' ');
104
+ }
105
+ return tokens.has(t);
106
+ }
68
107
  /**
69
108
  * Pick the right specialist for a given prompt.
70
109
  *
71
110
  * Algorithm:
72
- * 1. Walk routing rules in order.
73
- * 2. For the first rule whose triggers match, walk its `preferred` list.
74
- * 3. Return the first preferred adapter that's installed.
75
- * 4. If no rule matches OR no preferred adapter is installed, fall back to
76
- * `DEFAULT_SPECIALIST` if it's installed; otherwise the first-installed
77
- * adapter on the list.
111
+ * 1. Tokenize the prompt (whole-word matching, no substring false-positives).
112
+ * 2. Walk routing rules in order. Stop at the first rule whose triggers match.
113
+ * 3. Within that rule, return the first preferred adapter that's installed.
114
+ * 4. If that rule had a match but none of its preferred adapters are
115
+ * installed, return DEFAULT_SPECIALIST (or first installed) with a
116
+ * HONEST reason that names the matched rule and the fallback.
117
+ * 5. If no rule matched at all, return DEFAULT_SPECIALIST (or first
118
+ * installed) with a "no intent matched" reason.
78
119
  *
79
120
  * Returns null if NO specialist is installed (caller must handle).
121
+ *
122
+ * Known limitation: a prompt with multiple intents (e.g. "fix the bug and
123
+ * add tests") routes by the first matching rule, not the dominant intent.
124
+ * The v0.4 LLM classifier inherently handles dominance via confidence
125
+ * scoring; the rule-based path here keeps the deterministic-priority
126
+ * contract instead.
80
127
  */
81
128
  export function pickSpecialist(prompt, installedAgentIds) {
82
129
  if (installedAgentIds.length === 0) {
83
130
  return null;
84
131
  }
85
- const lowered = prompt.toLowerCase();
132
+ const installedSet = new Set(installedAgentIds);
133
+ const { tokens, normalized } = tokenize(prompt);
86
134
  for (const rule of ROUTING_RULES) {
87
- const matches = rule.triggers.some((t) => lowered.includes(t));
135
+ const matches = rule.triggers.some((t) => triggerMatches(t, tokens, normalized));
88
136
  if (!matches)
89
137
  continue;
138
+ // Rule matched. Walk preferred specialists in order.
90
139
  for (const candidate of rule.preferred) {
91
- if (installedAgentIds.includes(candidate)) {
140
+ if (installedSet.has(candidate)) {
92
141
  return { agentId: candidate, reason: rule.reason };
93
142
  }
94
143
  }
144
+ // Rule matched but none of its preferred specialists are installed.
145
+ // Fall through with an HONEST reason that names the matched rule + fallback,
146
+ // so the audit log records what actually happened (Codex review caught
147
+ // the prior version saying "default routing" here, which was a lie).
148
+ const fallbackId = installedSet.has(DEFAULT_SPECIALIST) ? DEFAULT_SPECIALIST : installedAgentIds[0];
149
+ return {
150
+ agentId: fallbackId,
151
+ reason: `${rule.reason} — but none of [${rule.preferred.join(', ')}] installed; falling back to ${fallbackId}`,
152
+ };
95
153
  }
96
- // No rule matched (or no preferred specialist installed) — default routing.
97
- if (installedAgentIds.includes(DEFAULT_SPECIALIST)) {
154
+ // No rule matched. Default routing.
155
+ if (installedSet.has(DEFAULT_SPECIALIST)) {
98
156
  return { agentId: DEFAULT_SPECIALIST, reason: 'default routing (no specific intent matched)' };
99
157
  }
100
158
  return { agentId: installedAgentIds[0], reason: 'fallback to first available specialist' };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codehere",
3
- "version": "0.3.0-rc.6",
3
+ "version": "0.3.0-rc.7",
4
4
  "description": "A PM for your AI coding agents. Delegate, orchestrate, and audit Claude Code, Codex, Aider, and OpenCode from one local web UI — every AI action traced, every file scored.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",