@infinitedusky/indusk-mcp 1.13.2 → 1.14.1
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/bin/cli.js +23 -0
- package/dist/bin/commands/extensions.js +38 -4
- package/dist/bin/commands/infra.js +1 -1
- package/dist/lib/beam/format.d.ts +7 -0
- package/dist/lib/beam/format.js +90 -0
- package/dist/lib/beam/graph-client.d.ts +18 -0
- package/dist/lib/beam/graph-client.js +41 -0
- package/dist/lib/beam/pipeline.d.ts +15 -0
- package/dist/lib/beam/pipeline.js +27 -0
- package/dist/lib/beam/queries/anchor-lookup.d.ts +6 -0
- package/dist/lib/beam/queries/anchor-lookup.js +32 -0
- package/dist/lib/beam/queries/cgc-relationships.d.ts +6 -0
- package/dist/lib/beam/queries/cgc-relationships.js +54 -0
- package/dist/lib/beam/queries/eval-findings.d.ts +6 -0
- package/dist/lib/beam/queries/eval-findings.js +32 -0
- package/dist/lib/beam/queries/neighbor-facts.d.ts +6 -0
- package/dist/lib/beam/queries/neighbor-facts.js +31 -0
- package/dist/lib/beam/queries/structural-neighbors.d.ts +6 -0
- package/dist/lib/beam/queries/structural-neighbors.js +41 -0
- package/dist/lib/beam/queries/target-facts.d.ts +6 -0
- package/dist/lib/beam/queries/target-facts.js +32 -0
- package/dist/lib/beam/runner.d.ts +14 -0
- package/dist/lib/beam/runner.js +145 -0
- package/dist/lib/beam/types.d.ts +49 -0
- package/dist/lib/beam/types.js +7 -0
- package/dist/lib/eval/prompt-builder.js +13 -9
- package/dist/lib/eval/rubric.js +5 -0
- package/dist/tools/graph-tools.js +38 -3
- package/extensions/graphiti/skill.md +2 -2
- package/package.json +1 -1
package/dist/bin/cli.js
CHANGED
|
@@ -290,6 +290,29 @@ eval_
|
|
|
290
290
|
const { evalBaseline } = await import("./commands/eval.js");
|
|
291
291
|
await evalBaseline(process.cwd(), opts);
|
|
292
292
|
});
|
|
293
|
+
program
|
|
294
|
+
.command("beam <file>")
|
|
295
|
+
.description("Get file-specific context from all sources")
|
|
296
|
+
.option("--trace", "Show query trace")
|
|
297
|
+
.option("--json", "Output as JSON")
|
|
298
|
+
.action(async (file, opts) => {
|
|
299
|
+
const { runBeam } = await import("../lib/beam/runner.js");
|
|
300
|
+
const { formatBeamMarkdown, formatBeamTrace } = await import("../lib/beam/format.js");
|
|
301
|
+
const result = await runBeam({
|
|
302
|
+
projectRoot: process.cwd(),
|
|
303
|
+
targetPath: file,
|
|
304
|
+
trace: opts.trace ?? false,
|
|
305
|
+
});
|
|
306
|
+
if (opts.json) {
|
|
307
|
+
console.info(JSON.stringify(result, null, 2));
|
|
308
|
+
}
|
|
309
|
+
else if (opts.trace) {
|
|
310
|
+
console.info(formatBeamTrace(result));
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
console.info(formatBeamMarkdown(result));
|
|
314
|
+
}
|
|
315
|
+
});
|
|
293
316
|
program
|
|
294
317
|
.command("serve")
|
|
295
318
|
.description("Start the MCP server (used by Claude Code via .mcp.json)")
|
|
@@ -115,7 +115,18 @@ export async function extensionsEnable(projectRoot, names) {
|
|
|
115
115
|
printMcpSetup(projectRoot, name);
|
|
116
116
|
continue;
|
|
117
117
|
}
|
|
118
|
-
|
|
118
|
+
// Try npm as last resort
|
|
119
|
+
console.info(` ${name}: not built-in, trying npm...`);
|
|
120
|
+
try {
|
|
121
|
+
await extensionsAdd(projectRoot, name, name);
|
|
122
|
+
if (isEnabled(projectRoot, name)) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// npm lookup failed, fall through
|
|
128
|
+
}
|
|
129
|
+
console.info(` ${name}: not found as built-in or npm package — use 'extensions add ${name} --from <source>' for third-party`);
|
|
119
130
|
}
|
|
120
131
|
}
|
|
121
132
|
export async function extensionsDisable(projectRoot, names) {
|
|
@@ -190,13 +201,36 @@ export async function extensionsAdd(projectRoot, name, from) {
|
|
|
190
201
|
}
|
|
191
202
|
}
|
|
192
203
|
else {
|
|
193
|
-
// Local path
|
|
204
|
+
// Local path — or fall back to npm if file doesn't exist
|
|
194
205
|
if (existsSync(from)) {
|
|
195
206
|
manifestContent = readFileSync(from, "utf-8");
|
|
196
207
|
}
|
|
197
208
|
else {
|
|
198
|
-
|
|
199
|
-
|
|
209
|
+
// Try as npm package name
|
|
210
|
+
const pkg = from;
|
|
211
|
+
try {
|
|
212
|
+
const tmpDir = join(projectRoot, ".indusk/tmp");
|
|
213
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
214
|
+
execSync(`npm pack ${pkg} --pack-destination "${tmpDir}"`, {
|
|
215
|
+
encoding: "utf-8",
|
|
216
|
+
timeout: 30000,
|
|
217
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
218
|
+
});
|
|
219
|
+
const tarballs = readdirSync(tmpDir).filter((f) => f.endsWith(".tgz"));
|
|
220
|
+
if (tarballs.length > 0) {
|
|
221
|
+
try {
|
|
222
|
+
manifestContent = execSync(`tar -xzf "${join(tmpDir, tarballs[tarballs.length - 1])}" -O package/indusk-extension.json`, { encoding: "utf-8", timeout: 10000 });
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
console.info(` ${name}: no indusk-extension.json found in npm package ${pkg}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
console.info(` ${name}: not found as local file or npm package: ${from}`);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
200
234
|
}
|
|
201
235
|
}
|
|
202
236
|
if (!manifestContent)
|
|
@@ -99,7 +99,7 @@ export async function infraStart() {
|
|
|
99
99
|
if (!hasImage) {
|
|
100
100
|
console.error(`Docker image '${IMAGE_NAME}' not found.`);
|
|
101
101
|
console.error("");
|
|
102
|
-
console.error("Build it from the
|
|
102
|
+
console.error("Build it from the dusk repo:");
|
|
103
103
|
console.error(" docker build -f docker/Dockerfile.infra -t indusk-infra .");
|
|
104
104
|
console.error("");
|
|
105
105
|
console.error("Or pull from GHCR (when published):");
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format beam results as markdown for agent consumption.
|
|
3
|
+
*/
|
|
4
|
+
import type { BeamResult } from "./types.js";
|
|
5
|
+
export declare function formatBeamMarkdown(result: BeamResult): string;
|
|
6
|
+
export declare function formatBeamCompact(result: BeamResult): string;
|
|
7
|
+
export declare function formatBeamTrace(result: BeamResult): string;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format beam results as markdown for agent consumption.
|
|
3
|
+
*/
|
|
4
|
+
export function formatBeamMarkdown(result) {
|
|
5
|
+
const lines = [];
|
|
6
|
+
lines.push(`## Context for ${result.target}\n`);
|
|
7
|
+
// Distance 0 items
|
|
8
|
+
const d0 = result.items.filter((i) => i.distance === 0);
|
|
9
|
+
if (d0.length > 0) {
|
|
10
|
+
lines.push("### This file (distance 0)\n");
|
|
11
|
+
for (const item of d0) {
|
|
12
|
+
const prefix = item.source === "eval" ? "⚠" : "•";
|
|
13
|
+
lines.push(`${prefix} **${item.source}**: ${item.content}`);
|
|
14
|
+
}
|
|
15
|
+
lines.push("");
|
|
16
|
+
}
|
|
17
|
+
// Distance 1 items grouped by source
|
|
18
|
+
const d1 = result.items.filter((i) => i.distance === 1);
|
|
19
|
+
if (d1.length > 0) {
|
|
20
|
+
const structural = d1.filter((i) => i.source === "semantic-graph");
|
|
21
|
+
const facts = d1.filter((i) => i.source === "graphiti");
|
|
22
|
+
const cgc = d1.filter((i) => i.source === "cgc");
|
|
23
|
+
if (structural.length > 0) {
|
|
24
|
+
lines.push("### Structural neighbors (distance 1)\n");
|
|
25
|
+
for (const item of structural) {
|
|
26
|
+
lines.push(`- ${item.content}`);
|
|
27
|
+
}
|
|
28
|
+
lines.push("");
|
|
29
|
+
}
|
|
30
|
+
if (facts.length > 0) {
|
|
31
|
+
lines.push("### Neighbor facts\n");
|
|
32
|
+
for (const item of facts) {
|
|
33
|
+
lines.push(`- ${item.content}`);
|
|
34
|
+
}
|
|
35
|
+
lines.push("");
|
|
36
|
+
}
|
|
37
|
+
if (cgc.length > 0) {
|
|
38
|
+
lines.push("### Function dependencies\n");
|
|
39
|
+
for (const item of cgc) {
|
|
40
|
+
lines.push(`- ${item.content}`);
|
|
41
|
+
}
|
|
42
|
+
lines.push("");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Distance 2 items
|
|
46
|
+
const d2 = result.items.filter((i) => i.distance === 2);
|
|
47
|
+
if (d2.length > 0) {
|
|
48
|
+
lines.push("### Extended context (distance 2)\n");
|
|
49
|
+
for (const item of d2) {
|
|
50
|
+
lines.push(`- ${item.content}`);
|
|
51
|
+
}
|
|
52
|
+
lines.push("");
|
|
53
|
+
}
|
|
54
|
+
lines.push(`*${result.items.length} items, ${result.durationMs}ms*`);
|
|
55
|
+
return lines.join("\n");
|
|
56
|
+
}
|
|
57
|
+
export function formatBeamCompact(result) {
|
|
58
|
+
const lines = [];
|
|
59
|
+
lines.push(`📡 [beam] ${result.target}`);
|
|
60
|
+
const d0 = result.items.filter((i) => i.distance === 0);
|
|
61
|
+
for (const item of d0) {
|
|
62
|
+
const prefix = item.source === "eval" ? " ⚠" : " •";
|
|
63
|
+
lines.push(`${prefix} ${item.content}`);
|
|
64
|
+
}
|
|
65
|
+
const neighbors = result.items
|
|
66
|
+
.filter((i) => i.distance === 1 && i.source === "semantic-graph")
|
|
67
|
+
.map((i) => i.metadata.path?.split("/").pop() ?? "")
|
|
68
|
+
.filter(Boolean);
|
|
69
|
+
if (neighbors.length > 0) {
|
|
70
|
+
lines.push(` → neighbors: ${neighbors.join(", ")}`);
|
|
71
|
+
}
|
|
72
|
+
return lines.join("\n");
|
|
73
|
+
}
|
|
74
|
+
export function formatBeamTrace(result) {
|
|
75
|
+
if (!result.trace)
|
|
76
|
+
return "(no trace data)";
|
|
77
|
+
const lines = [];
|
|
78
|
+
lines.push(`[beam] target: ${result.target}\n`);
|
|
79
|
+
for (const step of result.trace) {
|
|
80
|
+
lines.push(`[${step.query}] ${step.source} (${step.durationMs}ms)`);
|
|
81
|
+
lines.push(` → ${step.resultCount} results`);
|
|
82
|
+
for (const r of step.results) {
|
|
83
|
+
lines.push(` - ${r}`);
|
|
84
|
+
}
|
|
85
|
+
lines.push("");
|
|
86
|
+
}
|
|
87
|
+
lines.push(`[assembly] ${result.items.length} items total — ${result.items.filter((i) => i.distance === 0).length} high-signal (d0), ${result.items.filter((i) => i.distance === 1).length} awareness (d1)`);
|
|
88
|
+
lines.push(`[timing] ${result.durationMs}ms total`);
|
|
89
|
+
return lines.join("\n");
|
|
90
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight FalkorDB client for beam queries.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a connection to the semantic-{project} graph with a generic query
|
|
5
|
+
* method. The beam needs ad-hoc Cypher queries that don't fit the
|
|
6
|
+
* SemanticGraphClient's event-replay API.
|
|
7
|
+
*/
|
|
8
|
+
export declare class BeamGraphClient {
|
|
9
|
+
private readonly host;
|
|
10
|
+
private readonly port;
|
|
11
|
+
private db;
|
|
12
|
+
private graph;
|
|
13
|
+
private readonly graphName;
|
|
14
|
+
constructor(projectName: string, host?: string, port?: number);
|
|
15
|
+
connect(): Promise<void>;
|
|
16
|
+
query<T extends Record<string, unknown>>(cypher: string, params?: Record<string, unknown>): Promise<T[]>;
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight FalkorDB client for beam queries.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a connection to the semantic-{project} graph with a generic query
|
|
5
|
+
* method. The beam needs ad-hoc Cypher queries that don't fit the
|
|
6
|
+
* SemanticGraphClient's event-replay API.
|
|
7
|
+
*/
|
|
8
|
+
import { FalkorDB } from "falkordb";
|
|
9
|
+
const DEFAULT_HOST = "localhost";
|
|
10
|
+
const DEFAULT_PORT = 6379;
|
|
11
|
+
export class BeamGraphClient {
|
|
12
|
+
host;
|
|
13
|
+
port;
|
|
14
|
+
db;
|
|
15
|
+
graph;
|
|
16
|
+
graphName;
|
|
17
|
+
constructor(projectName, host = DEFAULT_HOST, port = DEFAULT_PORT) {
|
|
18
|
+
this.host = host;
|
|
19
|
+
this.port = port;
|
|
20
|
+
this.graphName = `semantic-${projectName}`;
|
|
21
|
+
}
|
|
22
|
+
async connect() {
|
|
23
|
+
if (this.db && this.graph)
|
|
24
|
+
return;
|
|
25
|
+
this.db = await FalkorDB.connect({ socket: { host: this.host, port: this.port } });
|
|
26
|
+
this.graph = this.db.selectGraph(this.graphName);
|
|
27
|
+
}
|
|
28
|
+
async query(cypher, params) {
|
|
29
|
+
if (!this.graph)
|
|
30
|
+
throw new Error("BeamGraphClient: call connect() first");
|
|
31
|
+
const result = await this.graph.query(cypher, params ? { params } : undefined);
|
|
32
|
+
return result.data ?? [];
|
|
33
|
+
}
|
|
34
|
+
async close() {
|
|
35
|
+
if (this.db) {
|
|
36
|
+
await this.db.close();
|
|
37
|
+
this.db = undefined;
|
|
38
|
+
this.graph = undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline definition — the ordered list of query steps.
|
|
3
|
+
*
|
|
4
|
+
* This is the single place to add, remove, or reorder queries.
|
|
5
|
+
* The runner executes this list; it doesn't know about specific data sources.
|
|
6
|
+
*/
|
|
7
|
+
import type { QueryStep } from "./types.js";
|
|
8
|
+
/**
|
|
9
|
+
* The beam pipeline. Each step is a discrete query.
|
|
10
|
+
*
|
|
11
|
+
* Execution groups (for parallelism):
|
|
12
|
+
* - Group A (independent): anchor-lookup, target-facts, eval-findings
|
|
13
|
+
* - Group B (depends on neighbors): structural-neighbors, then neighbor-facts, cgc-relationships
|
|
14
|
+
*/
|
|
15
|
+
export declare const BEAM_PIPELINE: QueryStep[];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline definition — the ordered list of query steps.
|
|
3
|
+
*
|
|
4
|
+
* This is the single place to add, remove, or reorder queries.
|
|
5
|
+
* The runner executes this list; it doesn't know about specific data sources.
|
|
6
|
+
*/
|
|
7
|
+
import { anchorLookup } from "./queries/anchor-lookup.js";
|
|
8
|
+
import { cgcRelationships } from "./queries/cgc-relationships.js";
|
|
9
|
+
import { evalFindings } from "./queries/eval-findings.js";
|
|
10
|
+
import { neighborFacts } from "./queries/neighbor-facts.js";
|
|
11
|
+
import { structuralNeighbors } from "./queries/structural-neighbors.js";
|
|
12
|
+
import { targetFacts } from "./queries/target-facts.js";
|
|
13
|
+
/**
|
|
14
|
+
* The beam pipeline. Each step is a discrete query.
|
|
15
|
+
*
|
|
16
|
+
* Execution groups (for parallelism):
|
|
17
|
+
* - Group A (independent): anchor-lookup, target-facts, eval-findings
|
|
18
|
+
* - Group B (depends on neighbors): structural-neighbors, then neighbor-facts, cgc-relationships
|
|
19
|
+
*/
|
|
20
|
+
export const BEAM_PIPELINE = [
|
|
21
|
+
anchorLookup,
|
|
22
|
+
structuralNeighbors,
|
|
23
|
+
targetFacts,
|
|
24
|
+
neighborFacts,
|
|
25
|
+
evalFindings,
|
|
26
|
+
cgcRelationships,
|
|
27
|
+
];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query 1: Find the target file's anchor in the semantic graph.
|
|
3
|
+
* Distance 0 — the starting point for the beam.
|
|
4
|
+
*/
|
|
5
|
+
import { BeamGraphClient } from "../graph-client.js";
|
|
6
|
+
export const anchorLookup = {
|
|
7
|
+
name: "anchor-lookup",
|
|
8
|
+
source: "semantic-graph",
|
|
9
|
+
distance: 0,
|
|
10
|
+
maxResults: 1,
|
|
11
|
+
detail: "full",
|
|
12
|
+
async execute(ctx) {
|
|
13
|
+
const client = new BeamGraphClient(ctx.projectName);
|
|
14
|
+
try {
|
|
15
|
+
await client.connect();
|
|
16
|
+
const rows = await client.query("MATCH (a:Anchor {path: $path, status: 'active'}) RETURN a.uuid AS uuid, a.kind AS kind, a.path AS path", { path: ctx.targetAbsolutePath });
|
|
17
|
+
return rows.map((row) => ({
|
|
18
|
+
source: "semantic-graph",
|
|
19
|
+
distance: 0,
|
|
20
|
+
detail: "full",
|
|
21
|
+
content: `Anchor: ${row.kind} at ${row.path}`,
|
|
22
|
+
metadata: { path: String(row.path) },
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
await client.close();
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query 6: CGC callers/callees — function-level dependencies.
|
|
3
|
+
* Distance 1, summary detail.
|
|
4
|
+
*/
|
|
5
|
+
import { FalkorDB } from "falkordb";
|
|
6
|
+
export const cgcRelationships = {
|
|
7
|
+
name: "cgc-relationships",
|
|
8
|
+
source: "cgc",
|
|
9
|
+
distance: 1,
|
|
10
|
+
maxResults: 10,
|
|
11
|
+
detail: "summary",
|
|
12
|
+
async execute(ctx) {
|
|
13
|
+
const graphName = `cgc-${ctx.projectName}`;
|
|
14
|
+
let db;
|
|
15
|
+
try {
|
|
16
|
+
db = await FalkorDB.connect({ socket: { host: "localhost", port: 6379 } });
|
|
17
|
+
const graph = db.selectGraph(graphName);
|
|
18
|
+
// Find functions in the target file and their callers/callees
|
|
19
|
+
// CGC IMPORTS: File → Module. CONTAINS: File → Function/Class.
|
|
20
|
+
// Find what this file imports (modules) and what files contain functions that call into this file.
|
|
21
|
+
const rows = await graph.query(`MATCH (f:File {relative_path: $path})-[r:IMPORTS]->(m:Module)
|
|
22
|
+
RETURN m.name AS name, 'imports' AS relationship, m.name AS path
|
|
23
|
+
UNION
|
|
24
|
+
MATCH (f:File {relative_path: $path})-[:CONTAINS]->(fn)-[:CALLS]->(callee)<-[:CONTAINS]-(other:File)
|
|
25
|
+
WHERE other.relative_path <> $path AND other.is_dependency = false
|
|
26
|
+
RETURN other.relative_path AS name, 'calls-into' AS relationship, other.relative_path AS path
|
|
27
|
+
UNION
|
|
28
|
+
MATCH (other:File)-[:CONTAINS]->(fn)-[:CALLS]->(callee)<-[:CONTAINS]-(f:File {relative_path: $path})
|
|
29
|
+
WHERE other.relative_path <> $path AND other.is_dependency = false
|
|
30
|
+
RETURN other.relative_path AS name, 'called-by' AS relationship, other.relative_path AS path`, { params: { path: ctx.targetRelativePath } });
|
|
31
|
+
const data = rows.data ?? [];
|
|
32
|
+
return data.map((row) => {
|
|
33
|
+
const neighborPath = String(row.path);
|
|
34
|
+
return {
|
|
35
|
+
source: "cgc",
|
|
36
|
+
distance: 1,
|
|
37
|
+
detail: "summary",
|
|
38
|
+
content: `${row.relationship}: ${neighborPath}`,
|
|
39
|
+
metadata: {
|
|
40
|
+
path: neighborPath,
|
|
41
|
+
relationship: String(row.relationship),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
if (db)
|
|
51
|
+
await db.close();
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query 5: Unresolved eval findings referencing the target file.
|
|
3
|
+
* Distance 0, full detail. These are active warnings.
|
|
4
|
+
*/
|
|
5
|
+
import { getUnresolvedFindings } from "../../eval/findings.js";
|
|
6
|
+
export const evalFindings = {
|
|
7
|
+
name: "eval-findings",
|
|
8
|
+
source: "eval",
|
|
9
|
+
distance: 0,
|
|
10
|
+
maxResults: 10,
|
|
11
|
+
detail: "full",
|
|
12
|
+
async execute(ctx) {
|
|
13
|
+
try {
|
|
14
|
+
const findings = getUnresolvedFindings(ctx.projectRoot);
|
|
15
|
+
const relevant = findings.filter((f) => f.finding.includes(ctx.targetPath) ||
|
|
16
|
+
f.finding.includes(ctx.targetPath.split("/").pop() ?? ""));
|
|
17
|
+
return relevant.map((f) => ({
|
|
18
|
+
source: "eval",
|
|
19
|
+
distance: 0,
|
|
20
|
+
detail: "full",
|
|
21
|
+
content: `[${f.severity}] ${f.questionId}: ${f.finding}`,
|
|
22
|
+
metadata: {
|
|
23
|
+
path: ctx.targetPath,
|
|
24
|
+
severity: f.severity,
|
|
25
|
+
},
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query 4: Graphiti facts for structural neighbors.
|
|
3
|
+
* Distance 1, summary detail.
|
|
4
|
+
*/
|
|
5
|
+
import { GraphitiClient } from "../../graphiti-client.js";
|
|
6
|
+
export const neighborFacts = {
|
|
7
|
+
name: "neighbor-facts",
|
|
8
|
+
source: "graphiti",
|
|
9
|
+
distance: 1,
|
|
10
|
+
maxResults: 5,
|
|
11
|
+
detail: "summary",
|
|
12
|
+
async execute(ctx) {
|
|
13
|
+
if (ctx.neighbors.length === 0)
|
|
14
|
+
return [];
|
|
15
|
+
try {
|
|
16
|
+
const client = new GraphitiClient(ctx.projectRoot);
|
|
17
|
+
const query = ctx.neighbors.slice(0, 10).join(", ");
|
|
18
|
+
const facts = await client.searchFacts(query, { maxResults: 5 });
|
|
19
|
+
return facts.map((f) => ({
|
|
20
|
+
source: "graphiti",
|
|
21
|
+
distance: 1,
|
|
22
|
+
detail: "summary",
|
|
23
|
+
content: f.fact ?? "(no fact text)",
|
|
24
|
+
metadata: { factId: f.uuid },
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query 2: Structural neighbors — files that import or are imported by the target.
|
|
3
|
+
* Distance 1. Populates ctx.neighbors for downstream queries.
|
|
4
|
+
*/
|
|
5
|
+
import { BeamGraphClient } from "../graph-client.js";
|
|
6
|
+
export const structuralNeighbors = {
|
|
7
|
+
name: "structural-neighbors",
|
|
8
|
+
source: "semantic-graph",
|
|
9
|
+
distance: 1,
|
|
10
|
+
maxResults: 20,
|
|
11
|
+
detail: "summary",
|
|
12
|
+
async execute(ctx) {
|
|
13
|
+
const client = new BeamGraphClient(ctx.projectName);
|
|
14
|
+
try {
|
|
15
|
+
await client.connect();
|
|
16
|
+
const rows = await client.query(`MATCH (a:Anchor {path: $path})-[r]-(neighbor:Anchor)
|
|
17
|
+
WHERE neighbor.status = 'active'
|
|
18
|
+
RETURN neighbor.path AS path, neighbor.kind AS kind, type(r) AS relationship,
|
|
19
|
+
COALESCE(neighbor.importance, 0) AS importance, COALESCE(r.weight, 1) AS weight
|
|
20
|
+
ORDER BY weight DESC, importance DESC`, { path: ctx.targetAbsolutePath });
|
|
21
|
+
// Populate neighbors on context for downstream queries
|
|
22
|
+
ctx.neighbors = rows.map((r) => String(r.path));
|
|
23
|
+
return rows.map((row) => ({
|
|
24
|
+
source: "semantic-graph",
|
|
25
|
+
distance: 1,
|
|
26
|
+
detail: "summary",
|
|
27
|
+
content: `${row.relationship}: ${row.path}`,
|
|
28
|
+
metadata: {
|
|
29
|
+
path: String(row.path),
|
|
30
|
+
relationship: String(row.relationship),
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
await client.close();
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query 3: Graphiti facts for the target file.
|
|
3
|
+
* Distance 0, full detail.
|
|
4
|
+
*/
|
|
5
|
+
import { GraphitiClient } from "../../graphiti-client.js";
|
|
6
|
+
export const targetFacts = {
|
|
7
|
+
name: "target-facts",
|
|
8
|
+
source: "graphiti",
|
|
9
|
+
distance: 0,
|
|
10
|
+
maxResults: 10,
|
|
11
|
+
detail: "full",
|
|
12
|
+
async execute(ctx) {
|
|
13
|
+
try {
|
|
14
|
+
const client = new GraphitiClient(ctx.projectRoot);
|
|
15
|
+
const facts = await client.searchFacts(ctx.targetPath, { maxResults: 10 });
|
|
16
|
+
return facts.map((f) => ({
|
|
17
|
+
source: "graphiti",
|
|
18
|
+
distance: 0,
|
|
19
|
+
detail: "full",
|
|
20
|
+
content: f.fact ?? "(no fact text)",
|
|
21
|
+
metadata: {
|
|
22
|
+
path: ctx.targetPath,
|
|
23
|
+
timestamp: f.valid_at ?? undefined,
|
|
24
|
+
factId: f.uuid,
|
|
25
|
+
},
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline runner — executes beam query steps and assembles results.
|
|
3
|
+
*
|
|
4
|
+
* Queries run in two groups:
|
|
5
|
+
* - Group A (independent): anchor-lookup, target-facts, eval-findings
|
|
6
|
+
* - Group B (needs neighbors): structural-neighbors first, then neighbor-facts, cgc-relationships
|
|
7
|
+
*/
|
|
8
|
+
import type { BeamResult } from "./types.js";
|
|
9
|
+
export interface BeamOptions {
|
|
10
|
+
projectRoot: string;
|
|
11
|
+
targetPath: string;
|
|
12
|
+
trace?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function runBeam(opts: BeamOptions): Promise<BeamResult>;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline runner — executes beam query steps and assembles results.
|
|
3
|
+
*
|
|
4
|
+
* Queries run in two groups:
|
|
5
|
+
* - Group A (independent): anchor-lookup, target-facts, eval-findings
|
|
6
|
+
* - Group B (needs neighbors): structural-neighbors first, then neighbor-facts, cgc-relationships
|
|
7
|
+
*/
|
|
8
|
+
import { basename, isAbsolute, join, relative } from "node:path";
|
|
9
|
+
import { BEAM_PIPELINE } from "./pipeline.js";
|
|
10
|
+
export async function runBeam(opts) {
|
|
11
|
+
const start = Date.now();
|
|
12
|
+
const projectName = basename(opts.projectRoot);
|
|
13
|
+
// Resolve both absolute and relative forms of the target path
|
|
14
|
+
const absolutePath = isAbsolute(opts.targetPath)
|
|
15
|
+
? opts.targetPath
|
|
16
|
+
: join(opts.projectRoot, opts.targetPath);
|
|
17
|
+
const relativePath = isAbsolute(opts.targetPath)
|
|
18
|
+
? relative(opts.projectRoot, opts.targetPath)
|
|
19
|
+
: opts.targetPath;
|
|
20
|
+
const ctx = {
|
|
21
|
+
projectRoot: opts.projectRoot,
|
|
22
|
+
projectName,
|
|
23
|
+
targetPath: opts.targetPath,
|
|
24
|
+
targetAbsolutePath: absolutePath,
|
|
25
|
+
targetRelativePath: relativePath,
|
|
26
|
+
neighbors: [],
|
|
27
|
+
trace: opts.trace ?? false,
|
|
28
|
+
};
|
|
29
|
+
const allItems = [];
|
|
30
|
+
const traceSteps = [];
|
|
31
|
+
// Group A: independent queries (anchor-lookup, target-facts, eval-findings)
|
|
32
|
+
const groupA = BEAM_PIPELINE.filter((s) => s.name === "anchor-lookup" || s.name === "target-facts" || s.name === "eval-findings");
|
|
33
|
+
// Group B1: structural-neighbors (must run before neighbor-facts and cgc)
|
|
34
|
+
const structuralStep = BEAM_PIPELINE.find((s) => s.name === "structural-neighbors");
|
|
35
|
+
// Group B2: depends on neighbors
|
|
36
|
+
const groupB2 = BEAM_PIPELINE.filter((s) => s.name === "neighbor-facts" || s.name === "cgc-relationships");
|
|
37
|
+
// Run Group A in parallel
|
|
38
|
+
const groupAResults = await Promise.all(groupA.map(async (step) => {
|
|
39
|
+
const stepStart = Date.now();
|
|
40
|
+
try {
|
|
41
|
+
const items = await step.execute(ctx);
|
|
42
|
+
if (ctx.trace) {
|
|
43
|
+
traceSteps.push({
|
|
44
|
+
query: step.name,
|
|
45
|
+
source: step.source,
|
|
46
|
+
durationMs: Date.now() - stepStart,
|
|
47
|
+
resultCount: items.length,
|
|
48
|
+
results: items.map((i) => i.content.slice(0, 100)),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return items;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
if (ctx.trace) {
|
|
55
|
+
traceSteps.push({
|
|
56
|
+
query: step.name,
|
|
57
|
+
source: step.source,
|
|
58
|
+
durationMs: Date.now() - stepStart,
|
|
59
|
+
resultCount: 0,
|
|
60
|
+
results: ["(query failed)"],
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}));
|
|
66
|
+
for (const items of groupAResults)
|
|
67
|
+
allItems.push(...items);
|
|
68
|
+
// Run structural-neighbors (populates ctx.neighbors)
|
|
69
|
+
if (structuralStep) {
|
|
70
|
+
const stepStart = Date.now();
|
|
71
|
+
try {
|
|
72
|
+
const items = await structuralStep.execute(ctx);
|
|
73
|
+
allItems.push(...items);
|
|
74
|
+
if (ctx.trace) {
|
|
75
|
+
traceSteps.push({
|
|
76
|
+
query: structuralStep.name,
|
|
77
|
+
source: structuralStep.source,
|
|
78
|
+
durationMs: Date.now() - stepStart,
|
|
79
|
+
resultCount: items.length,
|
|
80
|
+
results: items.map((i) => i.content.slice(0, 100)),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
if (ctx.trace) {
|
|
86
|
+
traceSteps.push({
|
|
87
|
+
query: structuralStep.name,
|
|
88
|
+
source: structuralStep.source,
|
|
89
|
+
durationMs: Date.now() - stepStart,
|
|
90
|
+
resultCount: 0,
|
|
91
|
+
results: ["(query failed)"],
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Run Group B2 in parallel (now that neighbors are populated)
|
|
97
|
+
const groupB2Results = await Promise.all(groupB2.map(async (step) => {
|
|
98
|
+
const stepStart = Date.now();
|
|
99
|
+
try {
|
|
100
|
+
const items = await step.execute(ctx);
|
|
101
|
+
if (ctx.trace) {
|
|
102
|
+
traceSteps.push({
|
|
103
|
+
query: step.name,
|
|
104
|
+
source: step.source,
|
|
105
|
+
durationMs: Date.now() - stepStart,
|
|
106
|
+
resultCount: items.length,
|
|
107
|
+
results: items.map((i) => i.content.slice(0, 100)),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return items;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
if (ctx.trace) {
|
|
114
|
+
traceSteps.push({
|
|
115
|
+
query: step.name,
|
|
116
|
+
source: step.source,
|
|
117
|
+
durationMs: Date.now() - stepStart,
|
|
118
|
+
resultCount: 0,
|
|
119
|
+
results: ["(query failed)"],
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
}));
|
|
125
|
+
for (const items of groupB2Results)
|
|
126
|
+
allItems.push(...items);
|
|
127
|
+
// Assembly: sort by distance (0 first), then eval findings first, then by timestamp (recent first)
|
|
128
|
+
allItems.sort((a, b) => {
|
|
129
|
+
if (a.distance !== b.distance)
|
|
130
|
+
return a.distance - b.distance;
|
|
131
|
+
if (a.source === "eval" && b.source !== "eval")
|
|
132
|
+
return -1;
|
|
133
|
+
if (b.source === "eval" && a.source !== "eval")
|
|
134
|
+
return 1;
|
|
135
|
+
const aTime = a.metadata.timestamp ? new Date(a.metadata.timestamp).getTime() : 0;
|
|
136
|
+
const bTime = b.metadata.timestamp ? new Date(b.metadata.timestamp).getTime() : 0;
|
|
137
|
+
return bTime - aTime;
|
|
138
|
+
});
|
|
139
|
+
return {
|
|
140
|
+
target: opts.targetPath,
|
|
141
|
+
items: allItems,
|
|
142
|
+
trace: ctx.trace ? traceSteps : undefined,
|
|
143
|
+
durationMs: Date.now() - start,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the context beam query pipeline.
|
|
3
|
+
*
|
|
4
|
+
* The beam radiates outward from a target file, collecting context from
|
|
5
|
+
* multiple data sources with distance-based relevance decay.
|
|
6
|
+
*/
|
|
7
|
+
export interface BeamItem {
|
|
8
|
+
source: "semantic-graph" | "graphiti" | "eval" | "cgc";
|
|
9
|
+
distance: 0 | 1 | 2;
|
|
10
|
+
detail: "full" | "summary" | "name";
|
|
11
|
+
content: string;
|
|
12
|
+
metadata: {
|
|
13
|
+
path?: string;
|
|
14
|
+
relationship?: string;
|
|
15
|
+
timestamp?: string;
|
|
16
|
+
severity?: string;
|
|
17
|
+
factId?: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export interface BeamTraceStep {
|
|
21
|
+
query: string;
|
|
22
|
+
source: string;
|
|
23
|
+
durationMs: number;
|
|
24
|
+
resultCount: number;
|
|
25
|
+
results: string[];
|
|
26
|
+
}
|
|
27
|
+
export interface BeamResult {
|
|
28
|
+
target: string;
|
|
29
|
+
items: BeamItem[];
|
|
30
|
+
trace?: BeamTraceStep[];
|
|
31
|
+
durationMs: number;
|
|
32
|
+
}
|
|
33
|
+
export interface QueryContext {
|
|
34
|
+
projectRoot: string;
|
|
35
|
+
projectName: string;
|
|
36
|
+
targetPath: string;
|
|
37
|
+
targetAbsolutePath: string;
|
|
38
|
+
targetRelativePath: string;
|
|
39
|
+
neighbors: string[];
|
|
40
|
+
trace: boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface QueryStep {
|
|
43
|
+
name: string;
|
|
44
|
+
source: "semantic-graph" | "graphiti" | "eval" | "cgc";
|
|
45
|
+
distance: 0 | 1 | 2;
|
|
46
|
+
maxResults: number;
|
|
47
|
+
detail: "full" | "summary" | "name";
|
|
48
|
+
execute: (ctx: QueryContext) => Promise<BeamItem[]>;
|
|
49
|
+
}
|
|
@@ -14,23 +14,27 @@ export function buildJudgePrompt(opts) {
|
|
|
14
14
|
.join("\n\n");
|
|
15
15
|
const graphitiInstructions = opts.mode === "eval"
|
|
16
16
|
? `
|
|
17
|
-
## Step 5: Write findings to
|
|
17
|
+
## Step 5: Write findings to the knowledge graph
|
|
18
18
|
|
|
19
|
-
For each finding with severity "warning" or "critical", write
|
|
19
|
+
For each finding with severity "warning" or "critical", write it using \`mcp__indusk__graph_capture\`. This dual-writes to both Graphiti AND the semantic graph, connecting the finding to the existing file anchor — so the context beam can find it later.
|
|
20
|
+
|
|
21
|
+
**Use \`graph_capture\`, NOT \`mcp__graphiti__add_memory\`.** graph_capture attaches the finding to the file's existing node in the graph. add_memory creates a disconnected episode.
|
|
22
|
+
|
|
23
|
+
For each finding, identify the **primary file** it relates to and pass it as \`file_path\`. Include all relevant file paths in the body text too.
|
|
20
24
|
|
|
21
25
|
\`\`\`
|
|
22
|
-
|
|
26
|
+
mcp__indusk__graph_capture({
|
|
23
27
|
name: "eval-finding-{question-id}-{short-slug}",
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
body: "In {file1} and {file2}: {finding text with evidence}",
|
|
29
|
+
file_path: "{primary file path}",
|
|
30
|
+
relation: "eval-finding",
|
|
31
|
+
group_id: "${opts.projectGroup}"
|
|
28
32
|
})
|
|
29
33
|
\`\`\`
|
|
30
34
|
|
|
31
35
|
Only write facts that would have changed the outcome. Be selective — quality over quantity.
|
|
32
|
-
Count how many
|
|
33
|
-
If
|
|
36
|
+
Count how many graph_capture calls you made for the scorecard.
|
|
37
|
+
If the tool is unavailable, skip silently and set graphitiWrites to 0.`
|
|
34
38
|
: `
|
|
35
39
|
## Step 5: Graphiti writes
|
|
36
40
|
|
package/dist/lib/eval/rubric.js
CHANGED
|
@@ -25,4 +25,9 @@ export const V1_RUBRIC = [
|
|
|
25
25
|
question: "Is there information missing from the graph that would have helped? (context sufficiency)",
|
|
26
26
|
guidance: "Consider what the agent struggled with or got wrong. Would a Graphiti fact, a lesson, or a CLAUDE.md entry have prevented the mistake?",
|
|
27
27
|
},
|
|
28
|
+
{
|
|
29
|
+
id: "user-intent",
|
|
30
|
+
question: "Did the user express decisions, concerns, reasoning, or preferences that should be captured in the knowledge graph?",
|
|
31
|
+
guidance: "Read the transcript for user statements that reveal WHY something was done, not just what. Look for: design reasoning ('the reason we do X is...'), concerns ('I'm worried about...'), preferences ('I don't like this DX'), constraints ('we need this for promotion'), corrections ('no, do it this way'). These are the most valuable knowledge — they're only in the transcript and will be lost if not captured. Write each one to the graph via graph_capture with the relevant file_path.",
|
|
32
|
+
},
|
|
28
33
|
];
|
|
@@ -575,9 +575,7 @@ export function registerGraphTools(server, projectRoot) {
|
|
|
575
575
|
logExists: existsSync(logPath),
|
|
576
576
|
logSize: `${(logSize / 1024).toFixed(1)}KB`,
|
|
577
577
|
eventCount,
|
|
578
|
-
lastSync: lastSyncTime
|
|
579
|
-
? { time: lastSyncTime, adapter: lastSyncAdapter }
|
|
580
|
-
: null,
|
|
578
|
+
lastSync: lastSyncTime ? { time: lastSyncTime, adapter: lastSyncAdapter } : null,
|
|
581
579
|
runtime: runtimeAvailable
|
|
582
580
|
? { anchors: anchorCount, edges: edgeCount }
|
|
583
581
|
: { error: "FalkorDB not available" },
|
|
@@ -646,4 +644,41 @@ export function registerGraphTools(server, projectRoot) {
|
|
|
646
644
|
};
|
|
647
645
|
}
|
|
648
646
|
});
|
|
647
|
+
server.registerTool("context_beam", {
|
|
648
|
+
description: "Get file-specific context from all sources (semantic graph, Graphiti, eval findings, CGC). Returns targeted, high-signal context for the file you're about to edit.",
|
|
649
|
+
inputSchema: {
|
|
650
|
+
path: z.string().describe("File path to get context for (relative to project root)"),
|
|
651
|
+
trace: z.boolean().default(false).describe("Show query trace"),
|
|
652
|
+
format: z.enum(["json", "markdown", "trace"]).default("markdown").describe("Output format"),
|
|
653
|
+
},
|
|
654
|
+
}, async ({ path: filePath, trace, format }) => {
|
|
655
|
+
try {
|
|
656
|
+
const { runBeam } = await import("../lib/beam/runner.js");
|
|
657
|
+
const { formatBeamMarkdown, formatBeamTrace } = await import("../lib/beam/format.js");
|
|
658
|
+
const result = await runBeam({
|
|
659
|
+
projectRoot,
|
|
660
|
+
targetPath: filePath,
|
|
661
|
+
trace: trace || format === "trace",
|
|
662
|
+
});
|
|
663
|
+
let text;
|
|
664
|
+
if (format === "json") {
|
|
665
|
+
text = JSON.stringify(result, null, 2);
|
|
666
|
+
}
|
|
667
|
+
else if (format === "trace") {
|
|
668
|
+
text = formatBeamTrace(result);
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
text = formatBeamMarkdown(result);
|
|
672
|
+
}
|
|
673
|
+
return { content: [{ type: "text", text }] };
|
|
674
|
+
}
|
|
675
|
+
catch (err) {
|
|
676
|
+
return {
|
|
677
|
+
content: [
|
|
678
|
+
{ type: "text", text: JSON.stringify({ error: err.message }) },
|
|
679
|
+
],
|
|
680
|
+
isError: true,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
});
|
|
649
684
|
}
|
|
@@ -34,7 +34,7 @@ Every episode belongs to a group. Groups isolate knowledge:
|
|
|
34
34
|
|
|
35
35
|
| Group | Purpose | Example |
|
|
36
36
|
|-------|---------|---------|
|
|
37
|
-
| `{project-name}` | Project-specific knowledge | `
|
|
37
|
+
| `{project-name}` | Project-specific knowledge | `dusk`, `numero` |
|
|
38
38
|
| `shared` | Cross-project conventions | Developer preferences, universal patterns |
|
|
39
39
|
|
|
40
40
|
When searching, always include both the project group and `shared` to get the full picture. Use `getProjectGroupId(projectRoot)` (from `apps/indusk-mcp/src/lib/config.ts`) to get the project group consistently — it reads `.indusk/config.json` `graphiti.groupId` if set, otherwise falls back to the project directory basename.
|
|
@@ -112,7 +112,7 @@ After a plan retrospective surfaces a useful insight, the retrospective skill ca
|
|
|
112
112
|
mcp__graphiti__add_memory({
|
|
113
113
|
name: "retro-gate-enforcement-1",
|
|
114
114
|
episode_body: "Plan gates need hook-based enforcement, not just instructions. The agent skipped gates when they were advisory only. PreToolUse hooks that block phase transitions are the fix.",
|
|
115
|
-
group_id: "
|
|
115
|
+
group_id: "dusk",
|
|
116
116
|
source: "text",
|
|
117
117
|
source_description: "retrospective insight"
|
|
118
118
|
})
|