gramatr 0.3.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.md +18 -0
- package/README.md +78 -0
- package/bin/clean-legacy-install.ts +28 -0
- package/bin/get-token.py +3 -0
- package/bin/gmtr-login.ts +547 -0
- package/bin/gramatr.js +33 -0
- package/bin/gramatr.ts +248 -0
- package/bin/install.ts +756 -0
- package/bin/render-claude-hooks.ts +16 -0
- package/bin/statusline.ts +437 -0
- package/bin/uninstall.ts +289 -0
- package/bin/version-sync.ts +46 -0
- package/codex/README.md +28 -0
- package/codex/hooks/session-start.ts +73 -0
- package/codex/hooks/stop.ts +34 -0
- package/codex/hooks/user-prompt-submit.ts +76 -0
- package/codex/install.ts +99 -0
- package/codex/lib/codex-hook-utils.ts +48 -0
- package/codex/lib/codex-install-utils.ts +123 -0
- package/core/feedback.ts +55 -0
- package/core/formatting.ts +167 -0
- package/core/install.ts +114 -0
- package/core/installer-cli.ts +122 -0
- package/core/migration.ts +244 -0
- package/core/routing.ts +98 -0
- package/core/session.ts +202 -0
- package/core/targets.ts +292 -0
- package/core/types.ts +178 -0
- package/core/version.ts +2 -0
- package/gemini/README.md +95 -0
- package/gemini/hooks/session-start.ts +72 -0
- package/gemini/hooks/stop.ts +30 -0
- package/gemini/hooks/user-prompt-submit.ts +74 -0
- package/gemini/install.ts +272 -0
- package/gemini/lib/gemini-hook-utils.ts +63 -0
- package/gemini/lib/gemini-install-utils.ts +169 -0
- package/hooks/GMTRPromptEnricher.hook.ts +650 -0
- package/hooks/GMTRRatingCapture.hook.ts +198 -0
- package/hooks/GMTRSecurityValidator.hook.ts +399 -0
- package/hooks/GMTRToolTracker.hook.ts +181 -0
- package/hooks/StopOrchestrator.hook.ts +78 -0
- package/hooks/gmtr-tool-tracker-utils.ts +105 -0
- package/hooks/lib/gmtr-hook-utils.ts +771 -0
- package/hooks/lib/identity.ts +227 -0
- package/hooks/lib/notify.ts +46 -0
- package/hooks/lib/paths.ts +104 -0
- package/hooks/lib/transcript-parser.ts +452 -0
- package/hooks/session-end.hook.ts +168 -0
- package/hooks/session-start.hook.ts +490 -0
- package/package.json +54 -0
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* GMTRPromptEnricher.hook.ts — grāmatr UserPromptSubmit Hook
|
|
4
|
+
*
|
|
5
|
+
* Fires before the agent processes a user prompt. Calls the grāmatr
|
|
6
|
+
* decision router to pre-classify the request, then injects
|
|
7
|
+
* the intelligence as additionalContext so the agent sees it.
|
|
8
|
+
*
|
|
9
|
+
* TRIGGER: UserPromptSubmit
|
|
10
|
+
* TIMEOUT: 15s default (configurable via GMTR_TIMEOUT)
|
|
11
|
+
* SAFETY: Never blocks. On any error, prompt passes through unmodified.
|
|
12
|
+
*
|
|
13
|
+
* What it injects:
|
|
14
|
+
* - Effort level (instant/fast/standard/extended/advanced/deep/comprehensive)
|
|
15
|
+
* - Intent type (search, retrieve, create, update, analyze, generate)
|
|
16
|
+
* - Matched skills from 25-capability registry
|
|
17
|
+
* - Reverse engineering (explicit/implicit wants and don't-wants)
|
|
18
|
+
* - ISC scaffold (preliminary Ideal State Criteria)
|
|
19
|
+
* - Suggested capabilities
|
|
20
|
+
* - Token savings metadata
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { getGitContext } from './lib/gmtr-hook-utils.ts';
|
|
24
|
+
import {
|
|
25
|
+
persistClassificationResult,
|
|
26
|
+
routePrompt,
|
|
27
|
+
shouldSkipPromptRouting,
|
|
28
|
+
} from '../core/routing.ts';
|
|
29
|
+
import type { RouteResponse } from '../core/types.ts';
|
|
30
|
+
|
|
31
|
+
// ── Types ──
|
|
32
|
+
|
|
33
|
+
interface HookInput {
|
|
34
|
+
session_id: string;
|
|
35
|
+
prompt: string;
|
|
36
|
+
cwd?: string;
|
|
37
|
+
permission_mode?: string;
|
|
38
|
+
hook_event_name: string;
|
|
39
|
+
transcript_path?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Configuration ──
|
|
43
|
+
|
|
44
|
+
const TIMEOUT_MS = parseInt(process.env.GMTR_TIMEOUT || '30000', 10);
|
|
45
|
+
const ENABLED = process.env.GMTR_ENRICH !== '0'; // disable with GMTR_ENRICH=0
|
|
46
|
+
|
|
47
|
+
// ── Project ID Resolution (Issue #76 — project-scoped memory) ──
|
|
48
|
+
|
|
49
|
+
function resolveProjectId(): string | null {
|
|
50
|
+
try {
|
|
51
|
+
// Read from the context file written by session-start.sh
|
|
52
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
53
|
+
const contextPath = `${home}/.claude/current-project-context.json`;
|
|
54
|
+
const context = JSON.parse(require('fs').readFileSync(contextPath, 'utf8'));
|
|
55
|
+
const remote = context.git_remote;
|
|
56
|
+
if (!remote || remote === 'no-remote') return null;
|
|
57
|
+
|
|
58
|
+
// Normalize git remote to org/repo format
|
|
59
|
+
// Handles: https://github.com/org/repo.git, git@github.com:org/repo.git, etc.
|
|
60
|
+
const match = remote.match(/[/:]([\w.-]+)\/([\w.-]+?)(?:\.git)?$/);
|
|
61
|
+
if (match) return `${match[1]}/${match[2]}`;
|
|
62
|
+
|
|
63
|
+
return null;
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const PROJECT_ID = resolveProjectId();
|
|
70
|
+
|
|
71
|
+
function persistLastClassification(
|
|
72
|
+
prompt: string,
|
|
73
|
+
sessionId: string,
|
|
74
|
+
route: RouteResponse | null,
|
|
75
|
+
downstreamModel: string,
|
|
76
|
+
): void {
|
|
77
|
+
try {
|
|
78
|
+
const git = getGitContext();
|
|
79
|
+
if (!git) return;
|
|
80
|
+
persistClassificationResult({
|
|
81
|
+
rootDir: git.root,
|
|
82
|
+
prompt,
|
|
83
|
+
route,
|
|
84
|
+
downstreamModel,
|
|
85
|
+
clientType: 'claude_code',
|
|
86
|
+
agentName: 'Claude Code',
|
|
87
|
+
});
|
|
88
|
+
} catch {
|
|
89
|
+
// Non-critical
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Failure Tracking ──
|
|
94
|
+
|
|
95
|
+
type RouterFailure = {
|
|
96
|
+
reason: 'auth' | 'timeout' | 'server_down' | 'server_error' | 'parse_error' | 'unknown';
|
|
97
|
+
detail: string;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
let lastFailure: RouterFailure | null = null;
|
|
101
|
+
|
|
102
|
+
// ── Format Failure Warning (LOUD — never silent) ──
|
|
103
|
+
|
|
104
|
+
function formatFailureWarning(failure: RouterFailure): string {
|
|
105
|
+
const lines: string[] = [];
|
|
106
|
+
lines.push('⚠️ [GMTR Intelligence — FAILED]');
|
|
107
|
+
lines.push('');
|
|
108
|
+
|
|
109
|
+
switch (failure.reason) {
|
|
110
|
+
case 'auth':
|
|
111
|
+
lines.push('🔒 AUTHENTICATION FAILURE — gramatr decision router cannot authenticate.');
|
|
112
|
+
lines.push(`Detail: ${failure.detail}`);
|
|
113
|
+
lines.push('');
|
|
114
|
+
lines.push('FIX: Set AIOS_MCP_TOKEN in your environment (same token as MCP server config)');
|
|
115
|
+
lines.push('Without this, NO pre-classification happens. The Algorithm runs without intelligence.');
|
|
116
|
+
break;
|
|
117
|
+
case 'timeout':
|
|
118
|
+
lines.push('⏱️ TIMEOUT — gramatr decision router did not respond in time.');
|
|
119
|
+
lines.push(`Detail: ${failure.detail}`);
|
|
120
|
+
lines.push('');
|
|
121
|
+
lines.push('The classifier may be overloaded or the server may be slow.');
|
|
122
|
+
break;
|
|
123
|
+
case 'server_down':
|
|
124
|
+
lines.push('🔴 SERVER UNREACHABLE — cannot connect to gramatr server.');
|
|
125
|
+
lines.push(`Detail: ${failure.detail}`);
|
|
126
|
+
lines.push('');
|
|
127
|
+
lines.push('Check: Is the server running? Is the URL correct? Is there a network issue?');
|
|
128
|
+
break;
|
|
129
|
+
case 'server_error':
|
|
130
|
+
lines.push('💥 SERVER ERROR — gramatr server returned an error.');
|
|
131
|
+
lines.push(`Detail: ${failure.detail}`);
|
|
132
|
+
break;
|
|
133
|
+
case 'parse_error':
|
|
134
|
+
lines.push('🔧 PARSE ERROR — could not understand the server response.');
|
|
135
|
+
lines.push(`Detail: ${failure.detail}`);
|
|
136
|
+
break;
|
|
137
|
+
default:
|
|
138
|
+
lines.push('❓ UNKNOWN ERROR — gramatr enrichment failed.');
|
|
139
|
+
lines.push(`Detail: ${failure.detail}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
lines.push('');
|
|
143
|
+
lines.push('IMPORTANT: Tell the user about this error. Do not silently proceed without intelligence.');
|
|
144
|
+
lines.push('You should still follow the Algorithm from CLAUDE.md, but note that pre-classification is unavailable.');
|
|
145
|
+
|
|
146
|
+
return lines.join('\n');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Format Intelligence Block ──
|
|
150
|
+
|
|
151
|
+
function formatIntelligence(data: RouteResponse): string {
|
|
152
|
+
const c = data.classification || {};
|
|
153
|
+
const ts = data.token_savings || {};
|
|
154
|
+
const es = data.execution_summary || {};
|
|
155
|
+
|
|
156
|
+
const lines: string[] = [];
|
|
157
|
+
const serverVersion = es.server_version || '';
|
|
158
|
+
const versionTag = serverVersion ? ` v${serverVersion}` : '';
|
|
159
|
+
lines.push(`[GMTR Intelligence — pre-classified by ${es.qwen_model || 'gramatr'}]`);
|
|
160
|
+
|
|
161
|
+
// ── PROJECT STATE (Issue #80 — render FIRST so agent can't miss it) ──
|
|
162
|
+
const ps = data.project_state;
|
|
163
|
+
if (ps && ps.project_id) {
|
|
164
|
+
lines.push('');
|
|
165
|
+
lines.push('ACTIVE PROJECT STATE:');
|
|
166
|
+
if (ps.active_prd_title) {
|
|
167
|
+
lines.push(` PRD: ${ps.active_prd_title}${ps.active_prd_id ? ` (${ps.active_prd_id})` : ''}`);
|
|
168
|
+
}
|
|
169
|
+
if (ps.current_phase) {
|
|
170
|
+
lines.push(` Phase: ${ps.current_phase}`);
|
|
171
|
+
}
|
|
172
|
+
if (ps.isc_summary && ps.isc_summary.total && ps.isc_summary.total > 0) {
|
|
173
|
+
const s = ps.isc_summary;
|
|
174
|
+
lines.push(` ISC: ${s.passing || 0}/${s.total} passing, ${s.failing || 0} failing, ${s.pending || 0} pending`);
|
|
175
|
+
}
|
|
176
|
+
if (ps.session_history_summary) {
|
|
177
|
+
lines.push(` Last session: "${ps.session_history_summary}"`);
|
|
178
|
+
}
|
|
179
|
+
if (ps.current_phase && ps.current_phase !== 'OBSERVE') {
|
|
180
|
+
lines.push(` ⚠️ RESUME from ${ps.current_phase} phase — do NOT restart OBSERVE`);
|
|
181
|
+
}
|
|
182
|
+
lines.push('');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Core classification
|
|
186
|
+
const meta: string[] = [];
|
|
187
|
+
if (c.effort_level) meta.push(`Effort: ${c.effort_level}`);
|
|
188
|
+
if (c.intent_type) meta.push(`Intent: ${c.intent_type}`);
|
|
189
|
+
if (c.confidence) meta.push(`Confidence: ${Math.round(c.confidence * 100)}%`);
|
|
190
|
+
if (c.memory_tier) meta.push(`Memory: ${c.memory_tier}`);
|
|
191
|
+
if (meta.length) lines.push(meta.join(' | '));
|
|
192
|
+
|
|
193
|
+
// Matched skills
|
|
194
|
+
if (c.matched_skills?.length) {
|
|
195
|
+
lines.push(`Matched skills: ${c.matched_skills.join(', ')}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Reverse engineering
|
|
199
|
+
const re = c.reverse_engineering;
|
|
200
|
+
if (re) {
|
|
201
|
+
if (re.explicit_wants?.length) {
|
|
202
|
+
lines.push('What user explicitly wants:');
|
|
203
|
+
for (const w of re.explicit_wants) lines.push(` - ${w}`);
|
|
204
|
+
}
|
|
205
|
+
if (re.implicit_wants?.length) {
|
|
206
|
+
lines.push('What is implied but not stated:');
|
|
207
|
+
for (const w of re.implicit_wants) lines.push(` - ${w}`);
|
|
208
|
+
}
|
|
209
|
+
if (re.explicit_dont_wants?.length) {
|
|
210
|
+
lines.push('What user explicitly does NOT want:');
|
|
211
|
+
for (const w of re.explicit_dont_wants) lines.push(` - ${w}`);
|
|
212
|
+
}
|
|
213
|
+
if (re.implicit_dont_wants?.length) {
|
|
214
|
+
lines.push('What user would clearly NOT want:');
|
|
215
|
+
for (const w of re.implicit_dont_wants) lines.push(` - ${w}`);
|
|
216
|
+
}
|
|
217
|
+
if (re.gotchas?.length) {
|
|
218
|
+
lines.push('Gotchas and edge cases:');
|
|
219
|
+
for (const g of re.gotchas) lines.push(` - ${g}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Suggested capabilities
|
|
224
|
+
if (c.suggested_capabilities?.length) {
|
|
225
|
+
lines.push(`Suggested capabilities: ${c.suggested_capabilities.join(', ')}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ISC scaffold
|
|
229
|
+
if (c.isc_scaffold?.length) {
|
|
230
|
+
lines.push('ISC Scaffold (preliminary Ideal State Criteria):');
|
|
231
|
+
for (let i = 0; i < c.isc_scaffold.length; i++) {
|
|
232
|
+
lines.push(` ${i + 1}. ${c.isc_scaffold[i]}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Constraints
|
|
237
|
+
if (c.constraints_extracted?.length) {
|
|
238
|
+
lines.push(`Constraints: ${c.constraints_extracted.join('; ')}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── Layer 3: Pre-computed intelligence (Issue #79) ──
|
|
242
|
+
|
|
243
|
+
// Capability audit — pre-computed USE/DECLINE/N/A for 25 capabilities
|
|
244
|
+
const audit = data.capability_audit;
|
|
245
|
+
if (audit?.formatted_summary) {
|
|
246
|
+
lines.push('');
|
|
247
|
+
lines.push(audit.formatted_summary);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Quality gate config — ISC validation rules as data
|
|
251
|
+
const qg = data.quality_gate_config;
|
|
252
|
+
if (qg?.rules?.length) {
|
|
253
|
+
lines.push('');
|
|
254
|
+
lines.push(`ISC Quality Gate: min ${qg.min_criteria || 4} criteria, ${qg.anti_required ? 'anti-criteria required' : 'anti-criteria optional'}, ${qg.word_range?.min || 8}-${qg.word_range?.max || 12} words each`);
|
|
255
|
+
const effortGated = qg.rules.filter(r => r.min_effort);
|
|
256
|
+
if (effortGated.length) {
|
|
257
|
+
lines.push(` Effort-gated rules: ${effortGated.map(r => `${r.id} (${r.min_effort}+)`).join(', ')}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Context pre-load plan — what entities to fetch by tier
|
|
262
|
+
const preload = data.context_pre_load_plan;
|
|
263
|
+
if (preload?.entity_types?.length) {
|
|
264
|
+
lines.push(`Context pre-load: ${preload.tier} tier → ${preload.entity_types.join(', ')}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── Behavioral Directives (from steering rules — the product value) ──
|
|
268
|
+
const directives = data.behavioral_directives;
|
|
269
|
+
if (directives?.length) {
|
|
270
|
+
lines.push('');
|
|
271
|
+
lines.push('BEHAVIORAL DIRECTIVES (from GMTR steering rules — follow these):');
|
|
272
|
+
for (const d of directives) lines.push(` - ${d}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ── Active Skill (server-delivered workflow — replaces client-side skill files) ──
|
|
276
|
+
const activeSkill = (data as any).active_skill;
|
|
277
|
+
if (activeSkill?.directives?.length) {
|
|
278
|
+
lines.push('');
|
|
279
|
+
lines.push(`ACTIVE SKILL: ${activeSkill.title || activeSkill.name} (phase: ${activeSkill.phase || 'ALL'})`);
|
|
280
|
+
for (const d of activeSkill.directives) lines.push(` - ${d}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── Behavioral Rules (server-delivered Algorithm framework — replaces CLAUDE.md) ──
|
|
284
|
+
const rules = (data as any).behavioral_rules;
|
|
285
|
+
const effort = c.effort_level || 'standard';
|
|
286
|
+
const intent = c.intent_type || 'analyze';
|
|
287
|
+
const memoryTier = c.memory_tier || 'none';
|
|
288
|
+
|
|
289
|
+
if (rules) {
|
|
290
|
+
// Server delivered the full framework — render from packet
|
|
291
|
+
lines.push('');
|
|
292
|
+
lines.push(`ALGORITHM: ${(rules.algorithm_phases || []).join(' → ')}`);
|
|
293
|
+
|
|
294
|
+
if (rules.hard_gates) {
|
|
295
|
+
lines.push('');
|
|
296
|
+
lines.push('HARD GATES:');
|
|
297
|
+
for (const [key, value] of Object.entries(rules.hard_gates)) {
|
|
298
|
+
lines.push(` - ${value}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (rules.verification_rules?.length) {
|
|
303
|
+
lines.push('');
|
|
304
|
+
lines.push('VERIFICATION RULES:');
|
|
305
|
+
for (const r of rules.verification_rules) lines.push(` - ${r}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (rules.code_rules?.length) {
|
|
309
|
+
lines.push('');
|
|
310
|
+
lines.push('CODE RULES:');
|
|
311
|
+
for (const r of rules.code_rules) lines.push(` - ${r}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (rules.safety_rules?.length) {
|
|
315
|
+
lines.push('');
|
|
316
|
+
lines.push('SAFETY RULES:');
|
|
317
|
+
for (const r of rules.safety_rules) lines.push(` - ${r}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ── MANDATORY GATES (always present regardless of effort level) ──
|
|
322
|
+
|
|
323
|
+
// Front-load memory query mandate when memory-dependent
|
|
324
|
+
if (memoryTier !== 'none') {
|
|
325
|
+
lines.push('');
|
|
326
|
+
lines.push('═══ MANDATORY: QUERY GMTR MEMORY BEFORE ANY WORK ═══');
|
|
327
|
+
lines.push('Memory tier: ' + memoryTier + ' — You MUST call search_semantic in OBSERVE before creating ISC.');
|
|
328
|
+
lines.push('Do NOT answer from MEMORY.md alone. GMTR has the live knowledge graph.');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
lines.push('');
|
|
332
|
+
lines.push('═══ MANDATORY: CREATE ISC VIA TaskCreate BEFORE ANY WORK ═══');
|
|
333
|
+
lines.push('You MUST call the TaskCreate tool for each criterion below. This creates visible tracked tasks.');
|
|
334
|
+
lines.push('NEVER write criteria as manual text/tables. ALWAYS use TaskCreate + TaskList tools.');
|
|
335
|
+
lines.push('ALWAYS prefix task subjects with "ISC-C{N}: " for criteria or "ISC-A{N}: " for anti-criteria.');
|
|
336
|
+
lines.push('The ISC prefix is REQUIRED — it signals to the user that GMTR intelligence is driving the criteria.');
|
|
337
|
+
lines.push('This is a HARD GATE — do NOT proceed to any work until TaskCreate calls are complete.');
|
|
338
|
+
|
|
339
|
+
// ── Effort-level-scaled format ──
|
|
340
|
+
if (effort === 'instant') {
|
|
341
|
+
lines.push('');
|
|
342
|
+
lines.push('FORMAT (instant effort — minimal): State, do, confirm.');
|
|
343
|
+
} else if (effort === 'fast') {
|
|
344
|
+
lines.push('');
|
|
345
|
+
lines.push('FORMAT (fast effort — compressed):');
|
|
346
|
+
lines.push(' 1. "Understanding: [wants] | Avoiding: [don\'t wants]"');
|
|
347
|
+
if (c.isc_scaffold?.length) {
|
|
348
|
+
lines.push(' 2. MANDATORY — call TaskCreate for each:');
|
|
349
|
+
for (let i = 0; i < c.isc_scaffold.length; i++) {
|
|
350
|
+
lines.push(` [INVOKE TaskCreate: subject="ISC-C${i + 1}: ${c.isc_scaffold[i]}", description="Binary testable: PASS or FAIL."]`);
|
|
351
|
+
}
|
|
352
|
+
lines.push(' 3. [INVOKE TaskList to display criteria to user]');
|
|
353
|
+
lines.push(' 4. Do the work');
|
|
354
|
+
lines.push(' 5. [INVOKE TaskList], then [INVOKE TaskUpdate] each with PASS/FAIL + evidence');
|
|
355
|
+
} else {
|
|
356
|
+
lines.push(' 2. [INVOKE TaskCreate for at least 4 criteria you identify]');
|
|
357
|
+
lines.push(' 3. [INVOKE TaskList to display criteria to user]');
|
|
358
|
+
lines.push(' 4. Do the work');
|
|
359
|
+
lines.push(' 5. [INVOKE TaskList], then [INVOKE TaskUpdate] each with PASS/FAIL + evidence');
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
// Standard+ — server behavioral_rules drives the framework
|
|
363
|
+
lines.push('');
|
|
364
|
+
lines.push(`FORMAT (${effort} effort — full phases)`);
|
|
365
|
+
|
|
366
|
+
// Suggested agents for the task
|
|
367
|
+
const agents = data.suggested_agents;
|
|
368
|
+
if (agents?.length) {
|
|
369
|
+
lines.push('');
|
|
370
|
+
lines.push('Suggested agents:');
|
|
371
|
+
for (const a of agents) {
|
|
372
|
+
lines.push(` - ${a.display_name || a.name || 'agent'} (${a.model || 'default'}) — ${a.reason || ''}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ── Memory Context (pre-loaded by router) ──
|
|
378
|
+
const mem = data.memory_context;
|
|
379
|
+
if (mem?.results?.length) {
|
|
380
|
+
lines.push('');
|
|
381
|
+
lines.push(`RELEVANT MEMORY (${mem.total_count} matches from GMTR knowledge graph):`);
|
|
382
|
+
for (const r of mem.results.slice(0, 5)) {
|
|
383
|
+
const sim = r.similarity ? ` (${Math.round(r.similarity * 100)}% match)` : '';
|
|
384
|
+
lines.push(` - [${r.entity_type || 'unknown'}] ${r.entity_name || 'unnamed'}${sim}: ${(r.content || '').substring(0, 150)}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ── Composed Agents (auto-composed by GMTR for this task) ──
|
|
389
|
+
const composed = data.composed_agents;
|
|
390
|
+
if (composed?.length) {
|
|
391
|
+
lines.push('');
|
|
392
|
+
lines.push('GMTR COMPOSED AGENT (specialized for this task — USE THIS):');
|
|
393
|
+
for (const ca of composed) {
|
|
394
|
+
lines.push(` Agent: ${ca.display_name || ca.name || 'specialist'}`);
|
|
395
|
+
lines.push(` Domain: ${ca.task_domain || 'general'} | Expertise: ${(ca.expertise_areas || []).join(', ')}`);
|
|
396
|
+
lines.push(` Model: ${ca.model_preference || 'default'}`);
|
|
397
|
+
lines.push(` Context: ${ca.context_summary || 'memory-aware'}`);
|
|
398
|
+
lines.push(' ACTION: Use the Task tool with subagent_type="general-purpose" and inject this system prompt:');
|
|
399
|
+
lines.push(' --- AGENT SYSTEM PROMPT START ---');
|
|
400
|
+
// Truncate to avoid overwhelming the context — full prompt available via gmtr_invoke_agent
|
|
401
|
+
const promptPreview = (ca.system_prompt || '').substring(0, 800);
|
|
402
|
+
lines.push(` ${promptPreview}${(ca.system_prompt || '').length > 800 ? '... [truncated — use gmtr_invoke_agent for full prompt]' : ''}`);
|
|
403
|
+
lines.push(' --- AGENT SYSTEM PROMPT END ---');
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Token economics
|
|
408
|
+
const saved = ts.total_saved || ts.tokens_saved || 0;
|
|
409
|
+
if (saved > 0) {
|
|
410
|
+
lines.push(`[Token savings: ${saved.toLocaleString()} tokens saved per request (CLAUDE.md: ${(ts.claude_md_reduction || 0).toLocaleString()}, OBSERVE offload: ${(ts.observe_work_offloaded || 0).toLocaleString()})]`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (data.curated_context) {
|
|
414
|
+
lines.push('');
|
|
415
|
+
lines.push('CURATED CONTEXT:');
|
|
416
|
+
lines.push(data.curated_context.trim());
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const diagnostics = data.packet_diagnostics;
|
|
420
|
+
if (diagnostics?.memory_context?.status === 'error' || diagnostics?.project_state?.status === 'error') {
|
|
421
|
+
lines.push('');
|
|
422
|
+
lines.push('PACKET DIAGNOSTICS:');
|
|
423
|
+
if (diagnostics.memory_context?.status === 'error') {
|
|
424
|
+
lines.push(` - Memory pre-load degraded: ${diagnostics.memory_context.error || 'unknown error'}`);
|
|
425
|
+
}
|
|
426
|
+
if (diagnostics.project_state?.status === 'error') {
|
|
427
|
+
lines.push(` - Project state degraded: ${diagnostics.project_state.error || 'unknown error'}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const degraded = data.execution_summary?.degraded_components?.filter((component) =>
|
|
432
|
+
component.startsWith('classification.')
|
|
433
|
+
) || [];
|
|
434
|
+
if (degraded.length > 0) {
|
|
435
|
+
lines.push('');
|
|
436
|
+
lines.push('CLASSIFIER DIAGNOSTICS:');
|
|
437
|
+
for (const component of degraded) {
|
|
438
|
+
lines.push(` - ${component.replace('classification.', '').replace(/_/g, ' ')} degraded`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return lines.join('\n');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ── Status Line (stderr) ──
|
|
446
|
+
|
|
447
|
+
function emitStatus(data: RouteResponse | null, elapsed: number): void {
|
|
448
|
+
if (!data) {
|
|
449
|
+
if (lastFailure) {
|
|
450
|
+
process.stderr.write(`[gramatr] ✗ ${lastFailure.reason} (${elapsed}ms) — ${lastFailure.detail}\n`);
|
|
451
|
+
} else {
|
|
452
|
+
process.stderr.write(`[gramatr] ✗ no result (${elapsed}ms)\n`);
|
|
453
|
+
}
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const c = data.classification || {};
|
|
458
|
+
const es = data.execution_summary || {};
|
|
459
|
+
const st = es.stage_timing || {};
|
|
460
|
+
const version = es.server_version ? `v${es.server_version}` : '';
|
|
461
|
+
|
|
462
|
+
// Summary line
|
|
463
|
+
const classifier = es.qwen_model || 'unknown';
|
|
464
|
+
const confidence = c.confidence ? `${Math.round(c.confidence * 100)}%` : '';
|
|
465
|
+
process.stderr.write(
|
|
466
|
+
`[grāmatr${version ? ' ' + version : ''}] ✓ ${c.effort_level || '?'}/${c.intent_type || '?'} ${confidence} (${classifier}, ${elapsed}ms)\n`
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
// Per-stage breakdown if available
|
|
470
|
+
const stages: string[] = [];
|
|
471
|
+
if (st.distilbert_ms !== undefined) stages.push(`classify:${st.distilbert_ms}ms`);
|
|
472
|
+
if (st.mistral_classify_ms !== undefined) stages.push(`classify:${st.mistral_classify_ms}ms`);
|
|
473
|
+
if (st.tool_calling_ms !== undefined) stages.push(`memory:${st.tool_calling_ms}ms`);
|
|
474
|
+
if (st.reverse_engineering_ms !== undefined) stages.push(`RE:${st.reverse_engineering_ms}ms`);
|
|
475
|
+
if (st.isc_scaffold_ms !== undefined) stages.push(`ISC:${st.isc_scaffold_ms}ms`);
|
|
476
|
+
if (stages.length > 0) {
|
|
477
|
+
process.stderr.write(`[gramatr] stages: ${stages.join(' → ')}\n`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ── Main ──
|
|
482
|
+
|
|
483
|
+
async function main() {
|
|
484
|
+
// Fast exit if disabled
|
|
485
|
+
if (!ENABLED) {
|
|
486
|
+
console.log(JSON.stringify({}));
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
let input: HookInput;
|
|
491
|
+
try {
|
|
492
|
+
let raw = '';
|
|
493
|
+
const chunks: Buffer[] = [];
|
|
494
|
+
await new Promise<void>((resolve) => {
|
|
495
|
+
const timeout = setTimeout(() => { process.stdin.destroy(); resolve(); }, 200);
|
|
496
|
+
process.stdin.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
497
|
+
process.stdin.on('end', () => { clearTimeout(timeout); resolve(); });
|
|
498
|
+
process.stdin.on('error', () => { clearTimeout(timeout); resolve(); });
|
|
499
|
+
process.stdin.resume();
|
|
500
|
+
});
|
|
501
|
+
raw = Buffer.concat(chunks).toString('utf8');
|
|
502
|
+
if (!raw.trim()) {
|
|
503
|
+
console.log(JSON.stringify({}));
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
input = JSON.parse(raw);
|
|
507
|
+
} catch {
|
|
508
|
+
console.log(JSON.stringify({}));
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const { prompt, session_id } = input;
|
|
513
|
+
if (!prompt) {
|
|
514
|
+
console.log(JSON.stringify({}));
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Skip trivial prompts — not worth the server call
|
|
519
|
+
if (shouldSkipPromptRouting(prompt)) {
|
|
520
|
+
process.stderr.write(`[gramatr] enricher: trivial prompt, skipped\n`);
|
|
521
|
+
console.log(JSON.stringify({}));
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Detect downstream LLM model from environment (#161)
|
|
526
|
+
// Claude Code exposes the model in the status line and hook input
|
|
527
|
+
const downstreamModel = process.env.ANTHROPIC_MODEL
|
|
528
|
+
|| process.env.CLAUDE_MODEL
|
|
529
|
+
|| (input as any).model
|
|
530
|
+
|| '';
|
|
531
|
+
|
|
532
|
+
// Emit pre-call status
|
|
533
|
+
process.stderr.write(`[gramatr] classifying...\n`);
|
|
534
|
+
|
|
535
|
+
// Call the router
|
|
536
|
+
const t0 = Date.now();
|
|
537
|
+
const routed = await routePrompt({
|
|
538
|
+
prompt,
|
|
539
|
+
projectId: PROJECT_ID || undefined,
|
|
540
|
+
sessionId: session_id,
|
|
541
|
+
timeoutMs: TIMEOUT_MS,
|
|
542
|
+
});
|
|
543
|
+
const result = routed.route;
|
|
544
|
+
const elapsed = Date.now() - t0;
|
|
545
|
+
|
|
546
|
+
if (!result && routed.error) {
|
|
547
|
+
switch (routed.error.reason) {
|
|
548
|
+
case 'auth':
|
|
549
|
+
lastFailure = { reason: 'auth', detail: routed.error.detail };
|
|
550
|
+
break;
|
|
551
|
+
case 'timeout':
|
|
552
|
+
lastFailure = { reason: 'timeout', detail: routed.error.detail };
|
|
553
|
+
break;
|
|
554
|
+
case 'network_error':
|
|
555
|
+
lastFailure = { reason: 'server_down', detail: routed.error.detail };
|
|
556
|
+
break;
|
|
557
|
+
case 'http_error':
|
|
558
|
+
case 'mcp_error':
|
|
559
|
+
lastFailure = { reason: 'server_error', detail: routed.error.detail };
|
|
560
|
+
break;
|
|
561
|
+
case 'parse_error':
|
|
562
|
+
lastFailure = { reason: 'parse_error', detail: routed.error.detail };
|
|
563
|
+
break;
|
|
564
|
+
default:
|
|
565
|
+
lastFailure = { reason: 'unknown', detail: routed.error.detail };
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
} else if (result) {
|
|
569
|
+
lastFailure = null;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Emit status to stderr
|
|
573
|
+
emitStatus(result, elapsed);
|
|
574
|
+
|
|
575
|
+
// Persist classification savings + downstream model for statusline and feedback
|
|
576
|
+
try {
|
|
577
|
+
const ts = result?.token_savings || {};
|
|
578
|
+
const es = result?.execution_summary || {};
|
|
579
|
+
const cl = result?.classification || {};
|
|
580
|
+
const savingsEntry = {
|
|
581
|
+
tokens_saved: ts.total_saved || ts.tokens_saved || 0,
|
|
582
|
+
savings_ratio: ts.savings_ratio || 0,
|
|
583
|
+
qwen_model: es.qwen_model || null,
|
|
584
|
+
qwen_time_ms: es.qwen_time_ms || 0,
|
|
585
|
+
effort: cl.effort_level || null,
|
|
586
|
+
intent: cl.intent_type || null,
|
|
587
|
+
confidence: cl.confidence || null,
|
|
588
|
+
memory_delivered: result?.memory_context?.results?.length || 0,
|
|
589
|
+
downstream_model: downstreamModel || null,
|
|
590
|
+
server_version: es.server_version || null,
|
|
591
|
+
stage_timing: es.stage_timing || null,
|
|
592
|
+
timestamp: Date.now(),
|
|
593
|
+
};
|
|
594
|
+
// Write latest classification savings
|
|
595
|
+
const { writeFileSync, appendFileSync, readFileSync } = await import('fs');
|
|
596
|
+
writeFileSync('/tmp/gmtr-classification-savings.json', JSON.stringify(savingsEntry));
|
|
597
|
+
// Append to history for cumulative tracking
|
|
598
|
+
const historyFile = '/tmp/gmtr-op-history.jsonl';
|
|
599
|
+
if (savingsEntry.tokens_saved > 0) {
|
|
600
|
+
const historyEntry = JSON.stringify({
|
|
601
|
+
tool: 'classification',
|
|
602
|
+
model: savingsEntry.qwen_model,
|
|
603
|
+
time_ms: savingsEntry.qwen_time_ms,
|
|
604
|
+
tokens_saved: savingsEntry.tokens_saved,
|
|
605
|
+
cache_hit: false,
|
|
606
|
+
timestamp: Date.now(),
|
|
607
|
+
});
|
|
608
|
+
appendFileSync(historyFile, historyEntry + '\n');
|
|
609
|
+
}
|
|
610
|
+
} catch {
|
|
611
|
+
// Non-critical — statusline just won't show savings
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
persistLastClassification(prompt, session_id, result, downstreamModel);
|
|
615
|
+
|
|
616
|
+
// If no result — DO NOT silently pass through. Tell the user what's broken.
|
|
617
|
+
if (!result || !result.classification) {
|
|
618
|
+
if (lastFailure) {
|
|
619
|
+
const errorContext = formatFailureWarning(lastFailure);
|
|
620
|
+
console.log(
|
|
621
|
+
JSON.stringify({
|
|
622
|
+
hookSpecificOutput: {
|
|
623
|
+
hookEventName: 'UserPromptSubmit',
|
|
624
|
+
additionalContext: errorContext,
|
|
625
|
+
},
|
|
626
|
+
})
|
|
627
|
+
);
|
|
628
|
+
} else {
|
|
629
|
+
console.log(JSON.stringify({}));
|
|
630
|
+
}
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Format and inject
|
|
635
|
+
const context = formatIntelligence(result);
|
|
636
|
+
|
|
637
|
+
console.log(
|
|
638
|
+
JSON.stringify({
|
|
639
|
+
hookSpecificOutput: {
|
|
640
|
+
hookEventName: 'UserPromptSubmit',
|
|
641
|
+
additionalContext: context,
|
|
642
|
+
},
|
|
643
|
+
})
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
main().catch(() => {
|
|
648
|
+
// Never crash, never block
|
|
649
|
+
console.log(JSON.stringify({}));
|
|
650
|
+
});
|