@cleocode/adapters 2026.4.48 → 2026.4.50
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/dist/cant-context.d.ts +132 -1
- package/dist/cant-context.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +204 -4
- package/dist/index.js.map +3 -3
- package/dist/providers/claude-code/hooks.d.ts.map +1 -1
- package/dist/providers/claude-code/spawn.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/cant-context.ts +397 -3
- package/src/index.ts +16 -2
- package/src/providers/claude-code/hooks.ts +7 -1
- package/src/providers/claude-code/spawn.ts +5 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../../src/providers/claude-code/hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AA8C/D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,sBAAuB,YAAW,mBAAmB;IAChE,kEAAkE;IAClE,OAAO,CAAC,UAAU,CAAS;IAE3B;;;;;;;;;;;OAWG;IACH,gBAAgB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAItD,+DAA+D;IAC/D,OAAO,CAAC,UAAU,CAAuB;IAEzC;;;;;;;;;;;;;;;;OAgBG;IACG,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../../src/providers/claude-code/hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AA8C/D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,sBAAuB,YAAW,mBAAmB;IAChE,kEAAkE;IAClE,OAAO,CAAC,UAAU,CAAS;IAE3B;;;;;;;;;;;OAWG;IACH,gBAAgB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAItD,+DAA+D;IAC/D,OAAO,CAAC,UAAU,CAAuB;IAEzC;;;;;;;;;;;;;;;;OAgBG;IACG,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA6E5D;;;;;;;OAOG;IACG,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IA2C5C;;OAEG;IACH,YAAY,IAAI,OAAO;IAIvB;;;;OAIG;IACH,aAAa,IAAI,MAAM,GAAG,IAAI;IAI9B;;;;;;;;OAQG;IACH,WAAW,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAI/C;;;;;;;;;;OAUG;IACG,2BAA2B,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAStD;;;;;;;;;;OAUG;IACG,kBAAkB,IAAI,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IASnD;;;;;;;;;;OAUG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAW9D;;;;;;;;;;;;OAYG;IACG,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CA2DrF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spawn.d.ts","sourceRoot":"","sources":["../../../src/providers/claude-code/spawn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAY3F;;;;;;;;;;;;;;GAcG;AACH,qBAAa,uBAAwB,YAAW,oBAAoB;IAClE,mDAAmD;IACnD,OAAO,CAAC,UAAU,CAAqC;IAEvD;;;;OAIG;IACG,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC;IASlC;;;;;;;;OAQG;IACG,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"spawn.d.ts","sourceRoot":"","sources":["../../../src/providers/claude-code/spawn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAY3F;;;;;;;;;;;;;;GAcG;AACH,qBAAa,uBAAwB,YAAW,oBAAoB;IAClE,mDAAmD;IACnD,OAAO,CAAC,UAAU,CAAqC;IAEvD;;;;OAIG;IACG,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC;IASlC;;;;;;;;OAQG;IACG,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;IAkGxD;;;;;;;OAOG;IACG,WAAW,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAqB3C;;;;;;;OAOG;IACG,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAWnD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cleocode/adapters",
|
|
3
|
-
"version": "2026.4.
|
|
3
|
+
"version": "2026.4.50",
|
|
4
4
|
"description": "Unified provider adapters for CLEO (Claude Code, OpenCode, Cursor)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@anthropic-ai/claude-agent-sdk": "0.2.108",
|
|
16
16
|
"@openai/agents": "0.8.3",
|
|
17
|
-
"@cleocode/caamp": "2026.4.
|
|
18
|
-
"@cleocode/contracts": "2026.4.
|
|
17
|
+
"@cleocode/caamp": "2026.4.50",
|
|
18
|
+
"@cleocode/contracts": "2026.4.50"
|
|
19
19
|
},
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"engines": {
|
package/src/cant-context.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* 1. Compiled CANT bundle (team topology, agent personas, tool ACLs)
|
|
9
9
|
* 2. Memory bridge (recent decisions, handoff notes, key patterns)
|
|
10
10
|
* 3. Mental model injection (validate-on-load agent-specific observations)
|
|
11
|
+
* 4. NEXUS code intelligence context (callers, callees, impact data) [T625]
|
|
11
12
|
*
|
|
12
13
|
* All operations are best-effort: if any step fails (missing packages, empty
|
|
13
14
|
* directories, compilation errors), the base prompt is returned unchanged.
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
* (Pi-only; this module generalizes the same logic for all providers)
|
|
18
19
|
*
|
|
19
20
|
* @task T555
|
|
21
|
+
* @task T625
|
|
20
22
|
*/
|
|
21
23
|
|
|
22
24
|
import { execFile } from 'node:child_process';
|
|
@@ -67,6 +69,69 @@ export interface BuildCantEnrichedPromptOptions {
|
|
|
67
69
|
* Use this to avoid double-compilation when the Pi bridge already compiled the bundle.
|
|
68
70
|
*/
|
|
69
71
|
compiledBundle?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Task ID to inject NEXUS code intelligence context for.
|
|
74
|
+
* When provided, step 6b fetches callers/callees/impact for symbols
|
|
75
|
+
* mentioned in the task description and injects them into the prompt.
|
|
76
|
+
*
|
|
77
|
+
* @task T625
|
|
78
|
+
*/
|
|
79
|
+
taskId?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// NEXUS context injection types (T625)
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
/** A single symbol's NEXUS context entry (callers, callees, impact). */
|
|
87
|
+
export interface NexusSymbolContext {
|
|
88
|
+
/** Symbol name as resolved in the code index. */
|
|
89
|
+
name: string;
|
|
90
|
+
/** Symbol kind (function, method, class, etc.). */
|
|
91
|
+
kind: string;
|
|
92
|
+
/** File path (relative to project root) where the symbol is defined. */
|
|
93
|
+
filePath: string | null;
|
|
94
|
+
/** Symbols that call this one (direct callers). */
|
|
95
|
+
callers: Array<{ name: string; kind: string; filePath: string | null }>;
|
|
96
|
+
/** Symbols that this one calls (direct callees). */
|
|
97
|
+
callees: Array<{ name: string; kind: string; filePath: string | null }>;
|
|
98
|
+
/** Risk level from impact analysis. */
|
|
99
|
+
riskLevel: string;
|
|
100
|
+
/** Total number of transitively impacted nodes. */
|
|
101
|
+
totalImpacted: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Options for {@link buildNexusContext}. */
|
|
105
|
+
export interface BuildNexusContextOptions {
|
|
106
|
+
/** Symbols to query NEXUS for (extracted from task description). */
|
|
107
|
+
symbols: string[];
|
|
108
|
+
/** Project root directory (used to resolve project ID and run CLI commands). */
|
|
109
|
+
projectDir: string;
|
|
110
|
+
/** Maximum callers/callees to include per symbol (default: 10). */
|
|
111
|
+
limit?: number;
|
|
112
|
+
/**
|
|
113
|
+
* CLI timeout in milliseconds for each `cleo nexus context` call (default: 10000).
|
|
114
|
+
*/
|
|
115
|
+
timeoutMs?: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Result of a post-modification NEXUS check (T625). */
|
|
119
|
+
export interface NexusModificationCheckResult {
|
|
120
|
+
/**
|
|
121
|
+
* True if the incremental re-analysis completed successfully.
|
|
122
|
+
* False if the analysis failed (non-fatal — agents should still continue).
|
|
123
|
+
*/
|
|
124
|
+
success: boolean;
|
|
125
|
+
/** Files that were re-indexed. */
|
|
126
|
+
reindexedFiles: string[];
|
|
127
|
+
/** Relations that exist in the post-check index but not in the snapshot. */
|
|
128
|
+
newRelations: number;
|
|
129
|
+
/** Relations that existed in the snapshot but are missing after re-analysis. */
|
|
130
|
+
removedRelations: number;
|
|
131
|
+
/** Human-readable summary of what changed. */
|
|
132
|
+
summary: string;
|
|
133
|
+
/** Any regressions detected (broken call chains, missing symbols). */
|
|
134
|
+
regressions: string[];
|
|
70
135
|
}
|
|
71
136
|
|
|
72
137
|
// ---------------------------------------------------------------------------
|
|
@@ -462,6 +527,320 @@ export async function buildIdentityBootstrap(projectDir: string): Promise<string
|
|
|
462
527
|
return sections.join('\n');
|
|
463
528
|
}
|
|
464
529
|
|
|
530
|
+
// ---------------------------------------------------------------------------
|
|
531
|
+
// NEXUS context injection (T625)
|
|
532
|
+
// ---------------------------------------------------------------------------
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Extract candidate symbol names from a task description or title.
|
|
536
|
+
*
|
|
537
|
+
* Uses a simple heuristic: camelCase, PascalCase, and snake_case tokens
|
|
538
|
+
* longer than 3 characters that look like code identifiers.
|
|
539
|
+
*
|
|
540
|
+
* Pure function — no I/O.
|
|
541
|
+
*
|
|
542
|
+
* @param text - Raw text to scan for symbol names (task title + description).
|
|
543
|
+
* @returns Deduplicated array of candidate symbol names, longest first.
|
|
544
|
+
*/
|
|
545
|
+
export function extractSymbolsFromText(text: string): string[] {
|
|
546
|
+
// Match camelCase, PascalCase, snake_case identifiers of length >= 4
|
|
547
|
+
const identifierPattern =
|
|
548
|
+
/\b([A-Z][a-zA-Z0-9]{3,}|[a-z][a-zA-Z0-9]{3,}[A-Z][a-zA-Z0-9]*|[a-z]{2,}_[a-z][a-zA-Z0-9_]*)\b/g;
|
|
549
|
+
const seen = new Set<string>();
|
|
550
|
+
const matches: string[] = [];
|
|
551
|
+
|
|
552
|
+
for (const matchResult of text.matchAll(identifierPattern)) {
|
|
553
|
+
const symbol = matchResult[1];
|
|
554
|
+
if (symbol && !seen.has(symbol)) {
|
|
555
|
+
seen.add(symbol);
|
|
556
|
+
matches.push(symbol);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Sort longest first (more specific symbols first)
|
|
561
|
+
return matches.sort((a, b) => b.length - a.length).slice(0, 8);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Build a NEXUS code intelligence context block for a set of symbols.
|
|
566
|
+
*
|
|
567
|
+
* Queries `cleo nexus context <symbol> --json` for each symbol and
|
|
568
|
+
* returns a formatted prompt block with callers, callees, and impact data.
|
|
569
|
+
*
|
|
570
|
+
* All CLI calls are best-effort with a configurable timeout. Failures
|
|
571
|
+
* for individual symbols are silently skipped.
|
|
572
|
+
*
|
|
573
|
+
* @param options - Symbols, project dir, and optional limits.
|
|
574
|
+
* @returns Array of resolved NEXUS symbol contexts.
|
|
575
|
+
* @task T625
|
|
576
|
+
*/
|
|
577
|
+
export async function buildNexusContext(
|
|
578
|
+
options: BuildNexusContextOptions,
|
|
579
|
+
): Promise<NexusSymbolContext[]> {
|
|
580
|
+
const { symbols, projectDir, limit = 10, timeoutMs = 10_000 } = options;
|
|
581
|
+
if (symbols.length === 0) return [];
|
|
582
|
+
|
|
583
|
+
const results: NexusSymbolContext[] = [];
|
|
584
|
+
|
|
585
|
+
for (const symbol of symbols) {
|
|
586
|
+
try {
|
|
587
|
+
const { stdout } = await execFileAsync(
|
|
588
|
+
'cleo',
|
|
589
|
+
['nexus', 'context', symbol, '--json', '--limit', String(limit)],
|
|
590
|
+
{ timeout: timeoutMs, cwd: projectDir || undefined },
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
const parsed = JSON.parse(stdout) as {
|
|
594
|
+
success?: boolean;
|
|
595
|
+
data?: {
|
|
596
|
+
results?: Array<{
|
|
597
|
+
name?: unknown;
|
|
598
|
+
kind?: unknown;
|
|
599
|
+
filePath?: unknown;
|
|
600
|
+
callers?: Array<{ name?: unknown; kind?: unknown; filePath?: unknown }>;
|
|
601
|
+
callees?: Array<{ name?: unknown; kind?: unknown; filePath?: unknown }>;
|
|
602
|
+
}>;
|
|
603
|
+
};
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
if (!parsed.success || !parsed.data?.results?.length) continue;
|
|
607
|
+
|
|
608
|
+
// Also fetch impact for risk classification
|
|
609
|
+
let riskLevel = 'UNKNOWN';
|
|
610
|
+
let totalImpacted = 0;
|
|
611
|
+
try {
|
|
612
|
+
const { stdout: impactStdout } = await execFileAsync(
|
|
613
|
+
'cleo',
|
|
614
|
+
['nexus', 'impact', symbol, '--json', '--depth', '2'],
|
|
615
|
+
{ timeout: timeoutMs, cwd: projectDir || undefined },
|
|
616
|
+
);
|
|
617
|
+
const impactParsed = JSON.parse(impactStdout) as {
|
|
618
|
+
success?: boolean;
|
|
619
|
+
data?: { riskLevel?: unknown; totalImpactedNodes?: unknown };
|
|
620
|
+
};
|
|
621
|
+
if (impactParsed.success && impactParsed.data) {
|
|
622
|
+
riskLevel = String(impactParsed.data.riskLevel ?? 'UNKNOWN');
|
|
623
|
+
totalImpacted = Number(impactParsed.data.totalImpactedNodes ?? 0);
|
|
624
|
+
}
|
|
625
|
+
} catch {
|
|
626
|
+
// Impact fetch failure — non-fatal, use defaults
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const primary = parsed.data.results[0];
|
|
630
|
+
if (!primary) continue;
|
|
631
|
+
|
|
632
|
+
results.push({
|
|
633
|
+
name: String(primary.name ?? symbol),
|
|
634
|
+
kind: String(primary.kind ?? 'unknown'),
|
|
635
|
+
filePath: primary.filePath ? String(primary.filePath) : null,
|
|
636
|
+
callers: (primary.callers ?? []).slice(0, limit).map((c) => ({
|
|
637
|
+
name: String(c.name ?? ''),
|
|
638
|
+
kind: String(c.kind ?? 'unknown'),
|
|
639
|
+
filePath: c.filePath ? String(c.filePath) : null,
|
|
640
|
+
})),
|
|
641
|
+
callees: (primary.callees ?? []).slice(0, limit).map((c) => ({
|
|
642
|
+
name: String(c.name ?? ''),
|
|
643
|
+
kind: String(c.kind ?? 'unknown'),
|
|
644
|
+
filePath: c.filePath ? String(c.filePath) : null,
|
|
645
|
+
})),
|
|
646
|
+
riskLevel,
|
|
647
|
+
totalImpacted,
|
|
648
|
+
});
|
|
649
|
+
} catch {
|
|
650
|
+
// Symbol not found or CLI unavailable — skip
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return results;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Format a NEXUS context array into a system-prompt block.
|
|
659
|
+
*
|
|
660
|
+
* Pure function — no I/O, safe to call in tests without a real DB.
|
|
661
|
+
*
|
|
662
|
+
* @param contexts - Resolved NEXUS symbol contexts.
|
|
663
|
+
* @returns Formatted prompt block, or empty string when contexts is empty.
|
|
664
|
+
* @task T625
|
|
665
|
+
*/
|
|
666
|
+
export function buildNexusContextBlock(contexts: NexusSymbolContext[]): string {
|
|
667
|
+
if (contexts.length === 0) return '';
|
|
668
|
+
|
|
669
|
+
const lines: string[] = [
|
|
670
|
+
'',
|
|
671
|
+
'===== NEXUS CODE INTELLIGENCE (pre-modification context) =====',
|
|
672
|
+
'Consult this before modifying any of these symbols.',
|
|
673
|
+
'High-risk symbols MUST be checked for callers before editing.',
|
|
674
|
+
'',
|
|
675
|
+
];
|
|
676
|
+
|
|
677
|
+
for (const ctx of contexts) {
|
|
678
|
+
lines.push(`## ${ctx.name} [${ctx.kind}]${ctx.filePath ? ` ${ctx.filePath}` : ''}`);
|
|
679
|
+
lines.push(` Risk: ${ctx.riskLevel} | Impacted nodes: ${ctx.totalImpacted}`);
|
|
680
|
+
|
|
681
|
+
if (ctx.callers.length > 0) {
|
|
682
|
+
lines.push(
|
|
683
|
+
` Callers (${ctx.callers.length}): ${ctx.callers.map((c) => c.name).join(', ')}`,
|
|
684
|
+
);
|
|
685
|
+
} else {
|
|
686
|
+
lines.push(' Callers: none');
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (ctx.callees.length > 0) {
|
|
690
|
+
lines.push(
|
|
691
|
+
` Callees (${ctx.callees.length}): ${ctx.callees.map((c) => c.name).join(', ')}`,
|
|
692
|
+
);
|
|
693
|
+
} else {
|
|
694
|
+
lines.push(' Callees: none');
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
lines.push('');
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
lines.push('===== END NEXUS CONTEXT =====');
|
|
701
|
+
return lines.join('\n');
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Inject NEXUS context for a task into the agent prompt.
|
|
706
|
+
*
|
|
707
|
+
* Fetches the task details from CLEO, extracts symbol names from the
|
|
708
|
+
* task title and description, then queries NEXUS for each symbol.
|
|
709
|
+
* Returns a formatted prompt block or empty string on any failure.
|
|
710
|
+
*
|
|
711
|
+
* @param taskId - CLEO task ID (e.g. "T625").
|
|
712
|
+
* @param projectDir - Project root directory.
|
|
713
|
+
* @returns Formatted NEXUS context block, or "" if unavailable.
|
|
714
|
+
* @task T625
|
|
715
|
+
*/
|
|
716
|
+
export async function buildNexusContextForTask(
|
|
717
|
+
taskId: string,
|
|
718
|
+
projectDir: string,
|
|
719
|
+
): Promise<string> {
|
|
720
|
+
try {
|
|
721
|
+
// Fetch task details to extract symbols
|
|
722
|
+
const { stdout: taskStdout } = await execFileAsync('cleo', ['show', taskId, '--json'], {
|
|
723
|
+
timeout: 8_000,
|
|
724
|
+
cwd: projectDir || undefined,
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
const taskData = JSON.parse(taskStdout) as {
|
|
728
|
+
data?: { title?: string; description?: string };
|
|
729
|
+
};
|
|
730
|
+
const title = taskData?.data?.title ?? '';
|
|
731
|
+
const description = taskData?.data?.description ?? '';
|
|
732
|
+
const combinedText = `${title} ${description}`;
|
|
733
|
+
|
|
734
|
+
const symbols = extractSymbolsFromText(combinedText);
|
|
735
|
+
if (symbols.length === 0) return '';
|
|
736
|
+
|
|
737
|
+
const contexts = await buildNexusContext({ symbols, projectDir });
|
|
738
|
+
return buildNexusContextBlock(contexts);
|
|
739
|
+
} catch {
|
|
740
|
+
return '';
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Run a post-modification NEXUS check on a set of changed files.
|
|
746
|
+
*
|
|
747
|
+
* Performs an incremental re-analysis of the changed files and compares
|
|
748
|
+
* relation counts before/after. Regressions (net loss of relations after
|
|
749
|
+
* modification) are flagged and returned for BRAIN storage.
|
|
750
|
+
*
|
|
751
|
+
* All operations are best-effort — never throws.
|
|
752
|
+
*
|
|
753
|
+
* @param changedFiles - Absolute paths to files that were modified.
|
|
754
|
+
* @param projectDir - Project root directory.
|
|
755
|
+
* @returns Check result with regression data.
|
|
756
|
+
* @task T625
|
|
757
|
+
*/
|
|
758
|
+
export async function runNexusPostModificationCheck(
|
|
759
|
+
changedFiles: string[],
|
|
760
|
+
projectDir: string,
|
|
761
|
+
): Promise<NexusModificationCheckResult> {
|
|
762
|
+
const empty: NexusModificationCheckResult = {
|
|
763
|
+
success: false,
|
|
764
|
+
reindexedFiles: [],
|
|
765
|
+
newRelations: 0,
|
|
766
|
+
removedRelations: 0,
|
|
767
|
+
summary: 'NEXUS post-check unavailable',
|
|
768
|
+
regressions: [],
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
if (changedFiles.length === 0) return empty;
|
|
772
|
+
|
|
773
|
+
try {
|
|
774
|
+
// 1. Snapshot current relation count before re-analysis
|
|
775
|
+
const { stdout: beforeStdout } = await execFileAsync('cleo', ['nexus', 'status', '--json'], {
|
|
776
|
+
timeout: 8_000,
|
|
777
|
+
cwd: projectDir || undefined,
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
let relationsBefore = 0;
|
|
781
|
+
try {
|
|
782
|
+
const beforeData = JSON.parse(beforeStdout) as {
|
|
783
|
+
data?: { relationCount?: number; relations?: number };
|
|
784
|
+
};
|
|
785
|
+
relationsBefore = beforeData?.data?.relationCount ?? beforeData?.data?.relations ?? 0;
|
|
786
|
+
} catch {
|
|
787
|
+
// Count unavailable — proceed anyway
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// 2. Run incremental analysis on the project (covers changed files)
|
|
791
|
+
await execFileAsync('cleo', ['nexus', 'analyze', projectDir, '--incremental', '--json'], {
|
|
792
|
+
timeout: 60_000,
|
|
793
|
+
cwd: projectDir || undefined,
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
// 3. Snapshot relation count after re-analysis
|
|
797
|
+
const { stdout: afterStdout } = await execFileAsync('cleo', ['nexus', 'status', '--json'], {
|
|
798
|
+
timeout: 8_000,
|
|
799
|
+
cwd: projectDir || undefined,
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
let relationsAfter = 0;
|
|
803
|
+
try {
|
|
804
|
+
const afterData = JSON.parse(afterStdout) as {
|
|
805
|
+
data?: { relationCount?: number; relations?: number };
|
|
806
|
+
};
|
|
807
|
+
relationsAfter = afterData?.data?.relationCount ?? afterData?.data?.relations ?? 0;
|
|
808
|
+
} catch {
|
|
809
|
+
// Count unavailable
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const delta = relationsAfter - relationsBefore;
|
|
813
|
+
const newRelations = Math.max(0, delta);
|
|
814
|
+
const removedRelations = Math.max(0, -delta);
|
|
815
|
+
const regressions: string[] = [];
|
|
816
|
+
|
|
817
|
+
// Flag significant relation loss as a potential regression
|
|
818
|
+
if (removedRelations > 5) {
|
|
819
|
+
regressions.push(
|
|
820
|
+
`Lost ${removedRelations} relations after modifying: ${changedFiles.map((f) => f.split('/').pop() ?? f).join(', ')}`,
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const summary =
|
|
825
|
+
delta === 0
|
|
826
|
+
? `NEXUS stable — no relation changes after modifying ${changedFiles.length} file(s)`
|
|
827
|
+
: delta > 0
|
|
828
|
+
? `NEXUS: +${newRelations} new relations from ${changedFiles.length} file(s)`
|
|
829
|
+
: `NEXUS: -${removedRelations} relations removed from ${changedFiles.length} file(s)${regressions.length > 0 ? ' — REGRESSION DETECTED' : ''}`;
|
|
830
|
+
|
|
831
|
+
return {
|
|
832
|
+
success: true,
|
|
833
|
+
reindexedFiles: changedFiles,
|
|
834
|
+
newRelations,
|
|
835
|
+
removedRelations,
|
|
836
|
+
summary,
|
|
837
|
+
regressions,
|
|
838
|
+
};
|
|
839
|
+
} catch {
|
|
840
|
+
return empty;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
465
844
|
// ---------------------------------------------------------------------------
|
|
466
845
|
// Main entry point
|
|
467
846
|
// ---------------------------------------------------------------------------
|
|
@@ -478,7 +857,8 @@ export async function buildIdentityBootstrap(projectDir: string): Promise<string
|
|
|
478
857
|
* 3. Renders the compiled system prompt
|
|
479
858
|
* 4. Reads the memory bridge from `.cleo/memory-bridge.md`
|
|
480
859
|
* 5. Fetches mental model observations for the named agent
|
|
481
|
-
*
|
|
860
|
+
* 6b. Injects NEXUS code intelligence context for the task (T625)
|
|
861
|
+
* 7. Concatenates: basePrompt + CANT bundle + memory bridge + mental model + NEXUS
|
|
482
862
|
*
|
|
483
863
|
* All operations are best-effort. If any step fails, the base prompt is
|
|
484
864
|
* returned unchanged. CANT context is an enrichment, not a gate — agents
|
|
@@ -490,7 +870,7 @@ export async function buildIdentityBootstrap(projectDir: string): Promise<string
|
|
|
490
870
|
export async function buildCantEnrichedPrompt(
|
|
491
871
|
options: BuildCantEnrichedPromptOptions,
|
|
492
872
|
): Promise<string> {
|
|
493
|
-
const { projectDir, basePrompt, agentName, isMainAgent, compiledBundle } = options;
|
|
873
|
+
const { projectDir, basePrompt, agentName, isMainAgent, compiledBundle, taskId } = options;
|
|
494
874
|
let appendix = '';
|
|
495
875
|
|
|
496
876
|
// Step 0: Identity bootstrap for the main session agent.
|
|
@@ -564,6 +944,20 @@ export async function buildCantEnrichedPrompt(
|
|
|
564
944
|
}
|
|
565
945
|
}
|
|
566
946
|
|
|
567
|
-
// Step
|
|
947
|
+
// Step 6b: Inject NEXUS code intelligence context for the task (T625).
|
|
948
|
+
// Extracts symbols from the task title+description, queries NEXUS for
|
|
949
|
+
// callers/callees/impact data, and injects as a pre-modification guide.
|
|
950
|
+
if (taskId) {
|
|
951
|
+
try {
|
|
952
|
+
const nexusBlock = await buildNexusContextForTask(taskId, projectDir);
|
|
953
|
+
if (nexusBlock) {
|
|
954
|
+
appendix += nexusBlock;
|
|
955
|
+
}
|
|
956
|
+
} catch {
|
|
957
|
+
// NEXUS context fetch failure — non-fatal
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Step 7: Return enriched prompt (or unchanged basePrompt if no context found)
|
|
568
962
|
return appendix ? basePrompt + appendix : basePrompt;
|
|
569
963
|
}
|
package/src/index.ts
CHANGED
|
@@ -13,9 +13,23 @@
|
|
|
13
13
|
* registry enable dynamic adapter loading by AdapterManager.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
export type {
|
|
16
|
+
export type {
|
|
17
|
+
BuildCantEnrichedPromptOptions,
|
|
18
|
+
BuildNexusContextOptions,
|
|
19
|
+
NexusModificationCheckResult,
|
|
20
|
+
NexusSymbolContext,
|
|
21
|
+
TierDiscoveryStats,
|
|
22
|
+
} from './cant-context.js';
|
|
17
23
|
// Shared CANT context builder — used by all spawn providers
|
|
18
|
-
export {
|
|
24
|
+
export {
|
|
25
|
+
buildCantEnrichedPrompt,
|
|
26
|
+
// NEXUS code intelligence helpers (T625)
|
|
27
|
+
buildNexusContext,
|
|
28
|
+
buildNexusContextBlock,
|
|
29
|
+
buildNexusContextForTask,
|
|
30
|
+
extractSymbolsFromText,
|
|
31
|
+
runNexusPostModificationCheck,
|
|
32
|
+
} from './cant-context.js';
|
|
19
33
|
// Re-export adapter classes for direct use
|
|
20
34
|
// Per-provider factory functions (renamed to avoid collisions)
|
|
21
35
|
export {
|
|
@@ -180,7 +180,7 @@ export class ClaudeCodeHookProvider implements AdapterHookProvider {
|
|
|
180
180
|
],
|
|
181
181
|
});
|
|
182
182
|
|
|
183
|
-
// Register PostToolUse hook → brain observation for file writes
|
|
183
|
+
// Register PostToolUse hook → brain observation for file writes + NEXUS post-check (T625)
|
|
184
184
|
if (!hooks.PostToolUse) hooks.PostToolUse = [];
|
|
185
185
|
(hooks.PostToolUse as unknown[]).push({
|
|
186
186
|
matcher: 'Write|Edit',
|
|
@@ -189,6 +189,12 @@ export class ClaudeCodeHookProvider implements AdapterHookProvider {
|
|
|
189
189
|
type: 'command',
|
|
190
190
|
command: `cleo observe "File modified via $TOOL_NAME" --title "tool-use" --quiet # cleo-hook`,
|
|
191
191
|
},
|
|
192
|
+
{
|
|
193
|
+
// NEXUS post-modification check: re-index changed files and flag regressions.
|
|
194
|
+
// $TOOL_INPUT_file_path is populated by Claude Code for Write/Edit events.
|
|
195
|
+
type: 'command',
|
|
196
|
+
command: `cleo nexus analyze --incremental --json > /dev/null 2>&1 && cleo observe "NEXUS re-indexed after $TOOL_NAME on $TOOL_INPUT_file_path" --title "nexus-post-check" --quiet # cleo-hook`,
|
|
197
|
+
},
|
|
192
198
|
],
|
|
193
199
|
});
|
|
194
200
|
|
|
@@ -74,7 +74,7 @@ export class ClaudeCodeSpawnProvider implements AdapterSpawnProvider {
|
|
|
74
74
|
let tmpFile: string | undefined;
|
|
75
75
|
|
|
76
76
|
try {
|
|
77
|
-
// Enrich prompt with CANT bundle, memory bridge,
|
|
77
|
+
// Enrich prompt with CANT bundle, memory bridge, mental model, and NEXUS context (T555, T625).
|
|
78
78
|
// Best-effort: if CANT context is unavailable, the raw prompt is used.
|
|
79
79
|
let enrichedPrompt = context.prompt;
|
|
80
80
|
try {
|
|
@@ -83,6 +83,10 @@ export class ClaudeCodeSpawnProvider implements AdapterSpawnProvider {
|
|
|
83
83
|
projectDir: context.workingDirectory ?? process.cwd(),
|
|
84
84
|
basePrompt: context.prompt,
|
|
85
85
|
agentName: (context.options?.agentName as string) ?? undefined,
|
|
86
|
+
// Inject NEXUS code intelligence context for the task scope (T625).
|
|
87
|
+
// This injects callers/callees/impact data so the agent understands
|
|
88
|
+
// blast radius before modifying any symbol.
|
|
89
|
+
taskId: context.taskId ?? undefined,
|
|
86
90
|
});
|
|
87
91
|
} catch {
|
|
88
92
|
// CANT enrichment unavailable — use raw prompt
|