@gramatr/client 0.6.19 → 0.6.21
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/core/routing.ts
CHANGED
|
@@ -47,6 +47,27 @@ export async function routePrompt(options: {
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Fetch Packet 2 enrichment (reverse engineering + ISC scaffold).
|
|
52
|
+
* Called automatically by the prompt enricher hook when packet_2_status is "pending".
|
|
53
|
+
* Brief timeout — enrichment is usually pre-computed by the time we ask.
|
|
54
|
+
*/
|
|
55
|
+
export async function fetchEnrichment(enrichmentId: string, timeoutMs: number = 2000): Promise<Record<string, unknown> | null> {
|
|
56
|
+
try {
|
|
57
|
+
const result = await callMcpToolDetailed<Record<string, unknown>>(
|
|
58
|
+
'gramatr_get_enrichment',
|
|
59
|
+
{ enrichment_id: enrichmentId, timeout_ms: timeoutMs },
|
|
60
|
+
timeoutMs + 1000, // HTTP timeout slightly longer than server timeout
|
|
61
|
+
);
|
|
62
|
+
if (result.data && result.data.status === 'ready') {
|
|
63
|
+
return result.data;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
50
71
|
export function describeRoutingFailure(error: MctToolCallError): {
|
|
51
72
|
title: string;
|
|
52
73
|
detail: string;
|
|
@@ -20,15 +20,22 @@
|
|
|
20
20
|
* - Token savings metadata
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import { readFileSync } from 'fs';
|
|
23
|
+
import { readFileSync, appendFileSync } from 'fs';
|
|
24
24
|
import { getGitContext } from './lib/gmtr-hook-utils.ts';
|
|
25
25
|
import {
|
|
26
26
|
persistClassificationResult,
|
|
27
27
|
routePrompt,
|
|
28
|
+
fetchEnrichment,
|
|
28
29
|
shouldSkipPromptRouting,
|
|
29
30
|
} from '../core/routing.ts';
|
|
30
31
|
import type { RouteResponse } from '../core/types.ts';
|
|
31
32
|
|
|
33
|
+
// ── Turn Tracking (Issue #594) ──
|
|
34
|
+
// Module-level counter incremented per prompt in this process.
|
|
35
|
+
// Reset on process restart (acceptable — session-end flushes).
|
|
36
|
+
let turnCounter = 0;
|
|
37
|
+
const TURNS_JSONL_PATH = '/tmp/gramatr-turns.jsonl';
|
|
38
|
+
|
|
32
39
|
// ── Types ──
|
|
33
40
|
|
|
34
41
|
interface HookInput {
|
|
@@ -149,7 +156,7 @@ function formatFailureWarning(failure: RouterFailure): string {
|
|
|
149
156
|
|
|
150
157
|
// ── Format Intelligence Block ──
|
|
151
158
|
|
|
152
|
-
function formatIntelligence(data: RouteResponse): string {
|
|
159
|
+
function formatIntelligence(data: RouteResponse, enrichment?: Record<string, unknown> | null): string {
|
|
153
160
|
const c = data.classification || {};
|
|
154
161
|
const ts = data.token_savings || {};
|
|
155
162
|
const es = data.execution_summary || {};
|
|
@@ -337,6 +344,10 @@ function formatIntelligence(data: RouteResponse): string {
|
|
|
337
344
|
lines.push('The ISC prefix is REQUIRED — it signals to the user that GMTR intelligence is driving the criteria.');
|
|
338
345
|
lines.push('This is a HARD GATE — do NOT proceed to any work until TaskCreate calls are complete.');
|
|
339
346
|
|
|
347
|
+
// ── grā identity — server-generated content marker ──
|
|
348
|
+
lines.push('');
|
|
349
|
+
lines.push('When presenting gramatr intelligence (ISC scaffolds, search results, agent recommendations, summaries), prefix with **grā:** in bold. This distinguishes server-pre-computed content from your own reasoning. Example: "**grā:** ISC scaffold suggests 6 criteria for this migration task."');
|
|
350
|
+
|
|
340
351
|
// ── Effort-level-scaled format ──
|
|
341
352
|
if (effort === 'instant') {
|
|
342
353
|
lines.push('');
|
|
@@ -440,6 +451,14 @@ function formatIntelligence(data: RouteResponse): string {
|
|
|
440
451
|
}
|
|
441
452
|
}
|
|
442
453
|
|
|
454
|
+
// Enrichment status — tell agent what happened with Packet 2
|
|
455
|
+
if (enrichment) {
|
|
456
|
+
// Enrichment was auto-fetched and merged — ISC scaffold + RE already in the output above
|
|
457
|
+
} else if (data.packet_2_status === 'pending' && data.enrichment_id) {
|
|
458
|
+
lines.push('');
|
|
459
|
+
lines.push(`Packet 2 (reverse engineering + ISC scaffold) is still generating. If needed, call gramatr_get_enrichment with enrichment_id="${data.enrichment_id}".`);
|
|
460
|
+
}
|
|
461
|
+
|
|
443
462
|
return lines.join('\n');
|
|
444
463
|
}
|
|
445
464
|
|
|
@@ -570,6 +589,29 @@ async function main() {
|
|
|
570
589
|
lastFailure = null;
|
|
571
590
|
}
|
|
572
591
|
|
|
592
|
+
// Auto-fetch Packet 2 enrichment if pending (reverse engineering + ISC scaffold)
|
|
593
|
+
// Brief wait — enrichment is usually pre-computed by the time Packet 1 returns.
|
|
594
|
+
// If it's not ready in 2s, inject what we have and tell the agent how to get it later.
|
|
595
|
+
let enrichment: Record<string, unknown> | null = null;
|
|
596
|
+
if (result && (result as any).packet_2_status === 'pending' && (result as any).enrichment_id) {
|
|
597
|
+
try {
|
|
598
|
+
enrichment = await fetchEnrichment((result as any).enrichment_id, 2000);
|
|
599
|
+
if (enrichment) {
|
|
600
|
+
// Merge enrichment into the classification so the existing formatting logic picks it up
|
|
601
|
+
const c = (result as any).classification;
|
|
602
|
+
if (c && enrichment.reverse_engineering) {
|
|
603
|
+
c.reverse_engineering = enrichment.reverse_engineering;
|
|
604
|
+
}
|
|
605
|
+
if (c && enrichment.isc_scaffold) {
|
|
606
|
+
c.isc_scaffold = enrichment.isc_scaffold;
|
|
607
|
+
}
|
|
608
|
+
if (enrichment.constraints_extracted) {
|
|
609
|
+
c.constraints_extracted = enrichment.constraints_extracted;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
} catch { /* non-blocking — Packet 1 still delivers */ }
|
|
613
|
+
}
|
|
614
|
+
|
|
573
615
|
// Emit status to stderr
|
|
574
616
|
emitStatus(result, elapsed);
|
|
575
617
|
|
|
@@ -614,6 +656,23 @@ async function main() {
|
|
|
614
656
|
|
|
615
657
|
persistLastClassification(prompt, session_id, result, downstreamModel);
|
|
616
658
|
|
|
659
|
+
// Accumulate turn data for batch flush at session end (Issue #594)
|
|
660
|
+
try {
|
|
661
|
+
const cl = result?.classification || {};
|
|
662
|
+
const es = result?.execution_summary || {};
|
|
663
|
+
const turnData = {
|
|
664
|
+
turn_number: turnCounter++,
|
|
665
|
+
timestamp: new Date().toISOString(),
|
|
666
|
+
prompt: prompt.substring(0, 500),
|
|
667
|
+
effort_level: cl.effort_level || null,
|
|
668
|
+
intent_type: cl.intent_type || null,
|
|
669
|
+
confidence: cl.confidence || null,
|
|
670
|
+
memory_tier: cl.memory_tier || null,
|
|
671
|
+
classifier_model: es.classifier_model || null,
|
|
672
|
+
};
|
|
673
|
+
appendFileSync(TURNS_JSONL_PATH, JSON.stringify(turnData) + '\n');
|
|
674
|
+
} catch { /* never block the hook */ }
|
|
675
|
+
|
|
617
676
|
// If no result — DO NOT silently pass through. Tell the user what's broken.
|
|
618
677
|
if (!result || !result.classification) {
|
|
619
678
|
if (lastFailure) {
|
|
@@ -633,7 +692,7 @@ async function main() {
|
|
|
633
692
|
}
|
|
634
693
|
|
|
635
694
|
// Format and inject
|
|
636
|
-
const context = formatIntelligence(result);
|
|
695
|
+
const context = formatIntelligence(result, enrichment);
|
|
637
696
|
|
|
638
697
|
console.log(
|
|
639
698
|
JSON.stringify({
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* ZERO external CLI dependencies — no jq, sed, curl, awk.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { writeFileSync } from 'fs';
|
|
14
|
+
import { writeFileSync, readFileSync, existsSync, unlinkSync } from 'fs';
|
|
15
15
|
import { join } from 'path';
|
|
16
16
|
import {
|
|
17
17
|
readHookInput,
|
|
@@ -129,6 +129,32 @@ async function main(): Promise<void> {
|
|
|
129
129
|
log('');
|
|
130
130
|
log('Saving session state to gramatr...');
|
|
131
131
|
|
|
132
|
+
// ── Flush accumulated turns to server (Issue #594) ──
|
|
133
|
+
try {
|
|
134
|
+
const turnsFile = '/tmp/gramatr-turns.jsonl';
|
|
135
|
+
if (existsSync(turnsFile)) {
|
|
136
|
+
const lines = readFileSync(turnsFile, 'utf8').trim().split('\n').filter(Boolean);
|
|
137
|
+
const turns = lines.map(line => JSON.parse(line));
|
|
138
|
+
if (turns.length > 0) {
|
|
139
|
+
log(` Flushing ${turns.length} turns to gramatr...`);
|
|
140
|
+
const interactionId = config?.current_session?.interaction_id || '';
|
|
141
|
+
const flushResult = await callMcpToolRaw('gramatr_batch_save_turns', {
|
|
142
|
+
session_id: sessionEntityId || sessionId,
|
|
143
|
+
project_id: projectId,
|
|
144
|
+
turns,
|
|
145
|
+
});
|
|
146
|
+
if (flushResult && flushResult.includes('"turns_saved"')) {
|
|
147
|
+
log(` Turns flushed successfully`);
|
|
148
|
+
} else {
|
|
149
|
+
log(' Turn flush uncertain (server may be unavailable)');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
unlinkSync(turnsFile);
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
log(' Turn flush failed (non-critical)');
|
|
156
|
+
}
|
|
157
|
+
|
|
132
158
|
// Session lifecycle only — gramatr_session_end records git summary on the session entity.
|
|
133
159
|
// Handoffs are saved by the AGENT in the LEARN phase via gramatr_save_handoff (HARD GATE).
|
|
134
160
|
// The hook does NOT save handoffs — it lacks conversation context.
|
|
@@ -482,8 +482,15 @@ async function main(): Promise<void> {
|
|
|
482
482
|
|
|
483
483
|
// Opportunistic npm version check — prints a notification to stderr if a
|
|
484
484
|
// newer gramatr is published. Never blocks or crashes the session start.
|
|
485
|
+
// Also injects context to stdout so Claude can proactively inform the user.
|
|
485
486
|
try {
|
|
486
|
-
await runVersionCheckAndNotify(VERSION);
|
|
487
|
+
const versionResult = await runVersionCheckAndNotify(VERSION);
|
|
488
|
+
if (versionResult?.isOutdated) {
|
|
489
|
+
emitStdout(`gramatr update available: v${versionResult.installedVersion} → v${versionResult.latestVersion}. ` +
|
|
490
|
+
`Inform the user that a gramatr update is available and recommend they exit Claude Code (/exit) ` +
|
|
491
|
+
`then run \`npx gramatr@latest install claude-code\` followed by \`claude --resume\` to get the latest features. ` +
|
|
492
|
+
`Hooks are loaded at session start so a restart is required for new hook code to take effect.`);
|
|
493
|
+
}
|
|
487
494
|
} catch {
|
|
488
495
|
// Silent — version check is strictly optional.
|
|
489
496
|
}
|