@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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gramatr/client",
3
- "version": "0.6.19",
3
+ "version": "0.6.21",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },