@infinitedusky/indusk-mcp 1.9.4 → 1.10.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/commands/extensions.js +17 -6
- package/dist/bin/commands/init.js +15 -0
- package/dist/bin/commands/update.js +32 -1
- package/dist/lib/config.d.ts +51 -0
- package/dist/lib/config.js +36 -1
- package/dist/lib/extension-loader.d.ts +1 -0
- package/dist/lib/extension-loader.js +1 -4
- package/extensions/excalidraw/manifest.json +1 -3
- package/extensions/graphiti/manifest.json +10 -0
- package/extensions/graphiti/skill.md +118 -26
- package/hooks/check-gates.js +51 -3
- package/hooks/validate-impl-structure.js +51 -5
- package/package.json +1 -1
- package/skills/catchup.md +24 -0
- package/skills/planner.md +28 -0
- package/skills/retrospective.md +30 -0
- package/skills/work.md +21 -0
|
@@ -3,7 +3,7 @@ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, write
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { globSync } from "glob";
|
|
6
|
-
import {
|
|
6
|
+
import { disabledDir, disableExtension, enableExtension, ensureExtensionsDirs, extensionConfigDir, extensionsDir, getEnabledExtensions, isEnabled, loadExtension, loadExtensions, resolveManifestPath, } from "../../lib/extension-loader.js";
|
|
7
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
8
|
const packageRoot = join(__dirname, "../../..");
|
|
9
9
|
const builtinDir = join(packageRoot, "extensions");
|
|
@@ -635,20 +635,31 @@ function printMcpInstructions(name, manifest) {
|
|
|
635
635
|
}
|
|
636
636
|
if (allResolved) {
|
|
637
637
|
const args = [
|
|
638
|
-
"mcp",
|
|
639
|
-
|
|
638
|
+
"mcp",
|
|
639
|
+
"add",
|
|
640
|
+
"-t",
|
|
641
|
+
"http",
|
|
642
|
+
...headerArgs.flatMap((h) => {
|
|
640
643
|
const match = h.match(/--header "(.+): (.+)"/);
|
|
641
644
|
if (match)
|
|
642
645
|
return ["--header", `${match[1]}: ${match[2]}`];
|
|
643
646
|
return [];
|
|
644
647
|
}),
|
|
645
|
-
"-s",
|
|
648
|
+
"-s",
|
|
649
|
+
"project",
|
|
650
|
+
"--",
|
|
651
|
+
name,
|
|
652
|
+
url,
|
|
646
653
|
];
|
|
647
|
-
const cmd = `claude ${args.map(a => a.includes(" ") ? `"${a}"` : a).join(" ")}`;
|
|
654
|
+
const cmd = `claude ${args.map((a) => (a.includes(" ") ? `"${a}"` : a)).join(" ")}`;
|
|
648
655
|
console.info(`\n ${name}: adding MCP server with credentials from .env...`);
|
|
649
656
|
// Remove existing entry first so we always write fresh credentials
|
|
650
657
|
try {
|
|
651
|
-
execSync(`claude mcp remove -s project ${name}`, {
|
|
658
|
+
execSync(`claude mcp remove -s project ${name}`, {
|
|
659
|
+
cwd: process.cwd(),
|
|
660
|
+
timeout: 10000,
|
|
661
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
662
|
+
});
|
|
652
663
|
}
|
|
653
664
|
catch {
|
|
654
665
|
// Ignore — may not exist
|
|
@@ -299,6 +299,21 @@ export async function init(projectRoot, options = {}) {
|
|
|
299
299
|
else {
|
|
300
300
|
console.info(" skip: codegraphcontext MCP server (already exists)");
|
|
301
301
|
}
|
|
302
|
+
// Add graphiti MCP server (Phase 5.5 — Streamable HTTP, runs in indusk-infra container)
|
|
303
|
+
const graphitiAddCommand = "claude mcp add -t http -s project graphiti http://localhost:8100/mcp";
|
|
304
|
+
if (!existingServers.has("graphiti") || force) {
|
|
305
|
+
try {
|
|
306
|
+
execSync(graphitiAddCommand, { cwd: projectRoot, stdio: "pipe", timeout: 10000 });
|
|
307
|
+
console.info(" added: graphiti MCP server (http://localhost:8100/mcp)");
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
console.info(" failed: could not add graphiti MCP server — run manually:");
|
|
311
|
+
console.info(` ${graphitiAddCommand}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
console.info(" skip: graphiti MCP server (already exists)");
|
|
316
|
+
}
|
|
302
317
|
// 4b. Check infrastructure container
|
|
303
318
|
console.info("\n[Infrastructure]");
|
|
304
319
|
try {
|
|
@@ -274,7 +274,38 @@ export async function update(projectRoot) {
|
|
|
274
274
|
console.info(` ${name}: update hook failed`);
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
|
-
|
|
277
|
+
// Phase 5.5: ensure declared MCP server is registered in .mcp.json.
|
|
278
|
+
// If the manifest's top-level mcp_server.add_command is set and the server
|
|
279
|
+
// is missing from .mcp.json, run the command. Idempotent — skips if present.
|
|
280
|
+
// The MCP server's name in .mcp.json is the extension name (matches init's
|
|
281
|
+
// `claude mcp add ... <extName> ...` convention).
|
|
282
|
+
const mcpServer = manifest?.mcp_server;
|
|
283
|
+
if (mcpServer?.add_command) {
|
|
284
|
+
const mcpJsonPath = join(projectRoot, ".mcp.json");
|
|
285
|
+
let alreadyRegistered = false;
|
|
286
|
+
if (existsSync(mcpJsonPath)) {
|
|
287
|
+
try {
|
|
288
|
+
const mcpJson = JSON.parse(readFileSync(mcpJsonPath, "utf-8"));
|
|
289
|
+
alreadyRegistered = !!mcpJson.mcpServers?.[name];
|
|
290
|
+
}
|
|
291
|
+
catch { }
|
|
292
|
+
}
|
|
293
|
+
if (!alreadyRegistered) {
|
|
294
|
+
try {
|
|
295
|
+
execSync(mcpServer.add_command, {
|
|
296
|
+
cwd: projectRoot,
|
|
297
|
+
stdio: "pipe",
|
|
298
|
+
timeout: 10000,
|
|
299
|
+
});
|
|
300
|
+
console.info(` ${name}: registered MCP server`);
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
console.info(` ${name}: failed to register MCP server — run manually:`);
|
|
304
|
+
console.info(` ${mcpServer.add_command}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
else if (mcpServer?.setup_instructions) {
|
|
278
309
|
console.info(` ${name}: MCP server setup — see .claude/skills/${name}/SKILL.md`);
|
|
279
310
|
}
|
|
280
311
|
}
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -14,8 +14,59 @@ export interface InduskConfig {
|
|
|
14
14
|
testRunner?: string;
|
|
15
15
|
linter?: string;
|
|
16
16
|
};
|
|
17
|
+
graphiti?: {
|
|
18
|
+
/**
|
|
19
|
+
* Group id used for project-specific Graphiti episodes. Defaults to the
|
|
20
|
+
* project directory basename. Override here if the directory name differs
|
|
21
|
+
* from the desired group id (e.g. shared monorepo, renamed project).
|
|
22
|
+
*/
|
|
23
|
+
groupId?: string;
|
|
24
|
+
};
|
|
25
|
+
otel?: {
|
|
26
|
+
/**
|
|
27
|
+
* Project's relationship to OpenTelemetry. Controls whether the OTel gate
|
|
28
|
+
* fires when the planner writes impl phases and when the validate-impl-structure
|
|
29
|
+
* / check-gates hooks evaluate them.
|
|
30
|
+
*
|
|
31
|
+
* - `service`: produces telemetry I want to collect (default behavior; gate fires)
|
|
32
|
+
* - `library`: ships to other people, never produces telemetry (gate silent)
|
|
33
|
+
* - `tool`: short-lived script, telemetry overhead exceeds value (gate silent)
|
|
34
|
+
* - `none`: explicit opt-out for legacy/prototype/internal experiments (gate silent)
|
|
35
|
+
*
|
|
36
|
+
* **If unset, behaves as `service`** (gate fires). This preserves backwards
|
|
37
|
+
* compatibility — existing projects without the field continue to get the OTel
|
|
38
|
+
* gate enforced. Opt-out is explicit; opt-in is implicit.
|
|
39
|
+
*/
|
|
40
|
+
role?: "service" | "library" | "tool" | "none";
|
|
41
|
+
};
|
|
17
42
|
}
|
|
18
43
|
export declare function getConfigPath(projectRoot: string): string;
|
|
19
44
|
export declare function readConfig(projectRoot: string): InduskConfig | null;
|
|
20
45
|
export declare function writeConfig(projectRoot: string, config: InduskConfig): void;
|
|
21
46
|
export declare function getPlanningDir(projectRoot: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* Get the Graphiti group id for project-specific episodes.
|
|
49
|
+
*
|
|
50
|
+
* Resolution order:
|
|
51
|
+
* 1. .indusk/config.json `graphiti.groupId` if set
|
|
52
|
+
* 2. Project directory basename
|
|
53
|
+
*
|
|
54
|
+
* Use `[getProjectGroupId(root), "shared"]` as the default group_ids list when
|
|
55
|
+
* searching Graphiti — this gives both project-scoped and cross-project knowledge.
|
|
56
|
+
*/
|
|
57
|
+
export declare function getProjectGroupId(projectRoot: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* Whether the OTel gate should fire for this project.
|
|
60
|
+
*
|
|
61
|
+
* Returns `true` if `.indusk/config.json` is missing, missing `otel.role`, or
|
|
62
|
+
* has `otel.role: "service"`. Returns `false` only when the project explicitly
|
|
63
|
+
* opts out via `otel.role: "library" | "tool" | "none"`.
|
|
64
|
+
*
|
|
65
|
+
* Used by:
|
|
66
|
+
* - planner skill (whether to write `#### Phase N OTel` sections into impl.md)
|
|
67
|
+
* - validate-impl-structure hook (whether to require an OTel section at write time)
|
|
68
|
+
* - check-gates hook (whether to block phase advancement on missing OTel)
|
|
69
|
+
*
|
|
70
|
+
* Backwards compatible: projects without the new field behave exactly as before.
|
|
71
|
+
*/
|
|
72
|
+
export declare function shouldEmitOtelGate(projectRoot: string): boolean;
|
package/dist/lib/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
3
|
const CONFIG_PATH = ".indusk/config.json";
|
|
4
4
|
export function getConfigPath(projectRoot) {
|
|
5
5
|
return join(projectRoot, CONFIG_PATH);
|
|
@@ -26,3 +26,38 @@ export function getPlanningDir(projectRoot) {
|
|
|
26
26
|
// Default to new path (will be created by init)
|
|
27
27
|
return newPath;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Get the Graphiti group id for project-specific episodes.
|
|
31
|
+
*
|
|
32
|
+
* Resolution order:
|
|
33
|
+
* 1. .indusk/config.json `graphiti.groupId` if set
|
|
34
|
+
* 2. Project directory basename
|
|
35
|
+
*
|
|
36
|
+
* Use `[getProjectGroupId(root), "shared"]` as the default group_ids list when
|
|
37
|
+
* searching Graphiti — this gives both project-scoped and cross-project knowledge.
|
|
38
|
+
*/
|
|
39
|
+
export function getProjectGroupId(projectRoot) {
|
|
40
|
+
const config = readConfig(projectRoot);
|
|
41
|
+
if (config?.graphiti?.groupId)
|
|
42
|
+
return config.graphiti.groupId;
|
|
43
|
+
return basename(projectRoot);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Whether the OTel gate should fire for this project.
|
|
47
|
+
*
|
|
48
|
+
* Returns `true` if `.indusk/config.json` is missing, missing `otel.role`, or
|
|
49
|
+
* has `otel.role: "service"`. Returns `false` only when the project explicitly
|
|
50
|
+
* opts out via `otel.role: "library" | "tool" | "none"`.
|
|
51
|
+
*
|
|
52
|
+
* Used by:
|
|
53
|
+
* - planner skill (whether to write `#### Phase N OTel` sections into impl.md)
|
|
54
|
+
* - validate-impl-structure hook (whether to require an OTel section at write time)
|
|
55
|
+
* - check-gates hook (whether to block phase advancement on missing OTel)
|
|
56
|
+
*
|
|
57
|
+
* Backwards compatible: projects without the new field behave exactly as before.
|
|
58
|
+
*/
|
|
59
|
+
export function shouldEmitOtelGate(projectRoot) {
|
|
60
|
+
const config = readConfig(projectRoot);
|
|
61
|
+
const role = config?.otel?.role;
|
|
62
|
+
return role === undefined || role === "service";
|
|
63
|
+
}
|
|
@@ -100,10 +100,7 @@ function loadFromDir(baseDir, enabled) {
|
|
|
100
100
|
export function loadExtensions(projectRoot) {
|
|
101
101
|
const dir = extensionsDir(projectRoot);
|
|
102
102
|
const disDir = disabledDir(projectRoot);
|
|
103
|
-
return [
|
|
104
|
-
...loadFromDir(dir, true),
|
|
105
|
-
...loadFromDir(disDir, false),
|
|
106
|
-
];
|
|
103
|
+
return [...loadFromDir(dir, true), ...loadFromDir(disDir, false)];
|
|
107
104
|
}
|
|
108
105
|
export function getEnabledExtensions(projectRoot) {
|
|
109
106
|
return loadExtensions(projectRoot).filter((e) => e.enabled);
|
|
@@ -7,8 +7,6 @@
|
|
|
7
7
|
"mcp_server": {
|
|
8
8
|
"type": "http",
|
|
9
9
|
"url": "https://mcp.excalidraw.com",
|
|
10
|
-
"setup_instructions": [
|
|
11
|
-
"claude mcp add -t http -- excalidraw https://mcp.excalidraw.com"
|
|
12
|
-
]
|
|
10
|
+
"setup_instructions": ["claude mcp add -t http -- excalidraw https://mcp.excalidraw.com"]
|
|
13
11
|
}
|
|
14
12
|
}
|
|
@@ -18,6 +18,16 @@
|
|
|
18
18
|
}
|
|
19
19
|
]
|
|
20
20
|
},
|
|
21
|
+
"mcp_server": {
|
|
22
|
+
"type": "http",
|
|
23
|
+
"url": "http://localhost:8100/mcp",
|
|
24
|
+
"add_command": "claude mcp add -t http -s project graphiti http://localhost:8100/mcp",
|
|
25
|
+
"setup_instructions": [
|
|
26
|
+
"Registered automatically by `indusk init` and re-added by `indusk update` if missing.",
|
|
27
|
+
"The Graphiti MCP server runs inside the `indusk-infra` container on port 8100.",
|
|
28
|
+
"Exposes 9 tools: add_memory, search_nodes, search_memory_facts, get_episodes, get_entity_edge, delete_episode, delete_entity_edge, clear_graph, get_status."
|
|
29
|
+
]
|
|
30
|
+
},
|
|
21
31
|
"detect": {
|
|
22
32
|
"command": "docker inspect --format='{{.State.Running}}' indusk-infra 2>/dev/null | grep true"
|
|
23
33
|
}
|
|
@@ -2,16 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
Graphiti is an episodic memory system backed by FalkorDB. It extracts entities and facts from text, detects contradictions, and supports semantic search across project-specific and shared knowledge.
|
|
4
4
|
|
|
5
|
+
The Graphiti MCP server runs inside the `indusk-infra` container on `http://localhost:8100/mcp` and is registered automatically by `indusk init`. The agent has direct access via `mcp__graphiti__*` tools — there is no wrapping layer.
|
|
6
|
+
|
|
5
7
|
## When to Use
|
|
6
8
|
|
|
7
|
-
- **
|
|
9
|
+
- **Capture**: After a decision, retro finding, or correction — anything worth remembering across sessions
|
|
8
10
|
- **Search**: Before making assumptions — check what's already known about a topic
|
|
9
|
-
- **
|
|
11
|
+
- **Recall**: At session start (`/catchup` does this automatically) or when working in an unfamiliar area
|
|
12
|
+
|
|
13
|
+
## Tools
|
|
14
|
+
|
|
15
|
+
The agent has nine `mcp__graphiti__*` tools. The five you'll use most:
|
|
16
|
+
|
|
17
|
+
| Tool | Purpose |
|
|
18
|
+
|------|---------|
|
|
19
|
+
| `add_memory` | Capture an episode (text/JSON/message) — entities and facts are extracted asynchronously |
|
|
20
|
+
| `search_nodes` | Find entities by natural-language query |
|
|
21
|
+
| `search_memory_facts` | Find facts (relationships between entities) by query |
|
|
22
|
+
| `get_episodes` | List recent episodes by group |
|
|
23
|
+
| `get_status` | Check that Graphiti is reachable and the database is healthy |
|
|
24
|
+
|
|
25
|
+
The other four (`delete_episode`, `delete_entity_edge`, `get_entity_edge`, `clear_graph`) are for cleanup — use sparingly, never in normal flow.
|
|
10
26
|
|
|
11
27
|
## Core Concepts
|
|
12
28
|
|
|
13
29
|
### Episodes
|
|
14
|
-
An episode is a chunk of text that Graphiti processes into entities and facts. Think of it as "something that happened" — a decision was made, a bug was found, a convention was established.
|
|
30
|
+
An episode is a chunk of text that Graphiti processes into entities and facts. Think of it as "something that happened" — a decision was made, a bug was found, a convention was established. Episodes are processed in the background; entities appear in search results once extraction completes (a few seconds).
|
|
15
31
|
|
|
16
32
|
### Group IDs
|
|
17
33
|
Every episode belongs to a group. Groups isolate knowledge:
|
|
@@ -21,54 +37,123 @@ Every episode belongs to a group. Groups isolate knowledge:
|
|
|
21
37
|
| `{project-name}` | Project-specific knowledge | `infinitedusky`, `numero` |
|
|
22
38
|
| `shared` | Cross-project conventions | Developer preferences, universal patterns |
|
|
23
39
|
|
|
24
|
-
When searching, always include both the project group and `shared` to get the full picture.
|
|
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.
|
|
25
41
|
|
|
26
42
|
### Entities and Facts
|
|
27
43
|
Graphiti extracts:
|
|
28
44
|
- **Entities**: Named things (tools, patterns, files, concepts)
|
|
29
45
|
- **Facts**: Relationships between entities with temporal validity
|
|
30
46
|
|
|
31
|
-
Facts can be contradicted — if you add "the parser handles three gate types" and later "the parser handles four gate types", Graphiti invalidates the old fact.
|
|
47
|
+
Facts can be contradicted — if you add "the parser handles three gate types" and later "the parser handles four gate types", Graphiti invalidates the old fact. The contradicted fact gets `invalid_at` set; the new one gets `valid_at` set. Search results respect this — invalid facts are excluded by default.
|
|
32
48
|
|
|
33
49
|
## Patterns
|
|
34
50
|
|
|
35
51
|
### Capturing a Decision
|
|
36
|
-
|
|
52
|
+
|
|
53
|
+
After an ADR is accepted or a significant choice is made, the planner skill calls this automatically. To do it manually:
|
|
54
|
+
|
|
37
55
|
```
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
56
|
+
mcp__graphiti__add_memory({
|
|
57
|
+
name: "auth-approach-decision",
|
|
58
|
+
episode_body: "We chose JWT with refresh tokens over session cookies because the API serves both web and mobile clients. Session cookies don't work well with React Native.",
|
|
59
|
+
group_id: "myproject",
|
|
60
|
+
source: "text",
|
|
61
|
+
source_description: "ADR"
|
|
62
|
+
})
|
|
41
63
|
```
|
|
42
64
|
|
|
65
|
+
The `name` should be short and topical — the agent uses it as a handle when re-discussing the decision. The `episode_body` should be detailed enough that someone reading it cold understands the decision and the reasoning.
|
|
66
|
+
|
|
43
67
|
### Capturing a Correction
|
|
44
|
-
|
|
68
|
+
|
|
69
|
+
When the user corrects the agent, the work skill prompts `context learn` AND captures the correction as an episode:
|
|
70
|
+
|
|
45
71
|
```
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
72
|
+
mcp__graphiti__add_memory({
|
|
73
|
+
name: "correction-pnpm-ce",
|
|
74
|
+
episode_body: "Always use `pnpm ce`, never `npx ce`. The composable.env skill specifies pnpm and the project's package.json maps `ce` to the binary. npx invokes a different code path.",
|
|
75
|
+
group_id: "shared",
|
|
76
|
+
source: "text",
|
|
77
|
+
source_description: "user correction"
|
|
78
|
+
})
|
|
49
79
|
```
|
|
50
80
|
|
|
81
|
+
Choosing `shared` vs project group:
|
|
82
|
+
- **`shared`**: tools, conventions, patterns that apply across projects ("always use pnpm ce", "never mock the database in integration tests")
|
|
83
|
+
- **`{project-name}`**: facts specific to one project's code, data, or domain ("the impl-parser handles four gate types per phase", "the bet matching engine is order-book based")
|
|
84
|
+
|
|
85
|
+
When in doubt, ask: "Would this correction make sense to a different project?" Yes → `shared`. No → project group.
|
|
86
|
+
|
|
51
87
|
### Searching Before Acting
|
|
88
|
+
|
|
52
89
|
Before making assumptions about how something works:
|
|
90
|
+
|
|
53
91
|
```
|
|
54
|
-
|
|
55
|
-
|
|
92
|
+
mcp__graphiti__search_nodes({
|
|
93
|
+
query: "authentication middleware",
|
|
94
|
+
group_ids: ["myproject", "shared"],
|
|
95
|
+
max_nodes: 10
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
mcp__graphiti__search_memory_facts({
|
|
99
|
+
query: "how does auth work in this project",
|
|
100
|
+
group_ids: ["myproject", "shared"],
|
|
101
|
+
max_facts: 10
|
|
102
|
+
})
|
|
56
103
|
```
|
|
57
104
|
|
|
105
|
+
Always search both the project group and `shared` — knowledge is split across them. The wrapper class `GraphitiClient` in `apps/indusk-mcp/src/lib/graphiti-client.ts` does this automatically when called internally; agents calling `mcp__graphiti__*` tools directly need to pass both group ids in the request.
|
|
106
|
+
|
|
58
107
|
### Capturing a Retrospective Finding
|
|
59
|
-
|
|
108
|
+
|
|
109
|
+
After a plan retrospective surfaces a useful insight, the retrospective skill captures it as an episode:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
mcp__graphiti__add_memory({
|
|
113
|
+
name: "retro-gate-enforcement-1",
|
|
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: "infinitedusky",
|
|
116
|
+
source: "text",
|
|
117
|
+
source_description: "retrospective insight"
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
One episode per insight, named `retro-{plan}-{n}`. If the retro surfaces a contradiction (we thought X, found Y), capture both framings as separate episodes — Graphiti's contradiction detection invalidates the older one.
|
|
122
|
+
|
|
123
|
+
### Recall at Session Start
|
|
124
|
+
|
|
125
|
+
The `/catchup` skill calls this automatically after reading project context, but you can also trigger it manually:
|
|
126
|
+
|
|
60
127
|
```
|
|
61
|
-
|
|
62
|
-
"
|
|
63
|
-
|
|
128
|
+
mcp__graphiti__search_nodes({
|
|
129
|
+
query: "recent decisions",
|
|
130
|
+
group_ids: ["myproject", "shared"],
|
|
131
|
+
max_nodes: 5
|
|
132
|
+
})
|
|
64
133
|
```
|
|
65
134
|
|
|
135
|
+
Surface anything notable to the user. Pay extra attention to facts where `invalid_at` is recent — those are areas where prior decisions changed and active plans may be working from stale assumptions.
|
|
136
|
+
|
|
137
|
+
## Capture Triggers (Where Episodes Come From)
|
|
138
|
+
|
|
139
|
+
In normal workflow, episodes are written automatically by other skills. The agent rarely calls `add_memory` directly:
|
|
140
|
+
|
|
141
|
+
| Trigger | Skill | Episode |
|
|
142
|
+
|---------|-------|---------|
|
|
143
|
+
| Brief accepted | `planner` | `brief-accepted-{plan}` — Problem + Proposed Direction in project group |
|
|
144
|
+
| ADR accepted | `planner` | `adr-{plan}` — Y-statement in project group |
|
|
145
|
+
| User correction (`context learn`) | `work` | `correction-{slug}` — lesson body in `shared` or project group |
|
|
146
|
+
| Retrospective lesson | `retrospective` | `retro-{plan}-{n}` — one per insight in project group |
|
|
147
|
+
| Retrospective "would do differently" | `retrospective` | `retro-{plan}-wdid-{n}` — one per item in project group |
|
|
148
|
+
|
|
149
|
+
The agent should call `add_memory` directly **only** when something worth remembering happens outside these trigger points. Most of the time, just trust the skills to capture and let `/catchup` recall.
|
|
150
|
+
|
|
66
151
|
## What NOT to Capture
|
|
67
152
|
|
|
68
|
-
- Code structure
|
|
69
|
-
- Git history
|
|
70
|
-
- Ephemeral state
|
|
71
|
-
- Things already in CLAUDE.md or lessons
|
|
153
|
+
- **Code structure** — CGC handles this. `function X calls function Y` is a graph relationship, not an episode.
|
|
154
|
+
- **Git history** — `git log` is authoritative.
|
|
155
|
+
- **Ephemeral state** — current task, in-progress work, todo lists. Use the TodoWrite tool, not Graphiti.
|
|
156
|
+
- **Things already in CLAUDE.md or lessons** — those layers exist for stable, project-wide truths. Graphiti is for things that have temporal context and might be contradicted.
|
|
72
157
|
|
|
73
158
|
Graphiti is for knowledge that has temporal context — decisions that might change, facts that might be contradicted, insights that accumulate over time.
|
|
74
159
|
|
|
@@ -86,6 +171,13 @@ Global config (API keys, OTel): `~/.indusk/config.env`
|
|
|
86
171
|
|
|
87
172
|
### Graceful Degradation
|
|
88
173
|
If the `indusk-infra` container is down:
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
- The
|
|
174
|
+
- `mcp__graphiti__*` tools will report a clean error (the MCP transport fails to connect)
|
|
175
|
+
- The agent should fall back to flat-file context (CLAUDE.md, lessons, skills) and continue working
|
|
176
|
+
- The `GraphitiClient` wrapper class (used internally by indusk-mcp) returns null/empty instead of throwing
|
|
177
|
+
- Don't pretend the call succeeded — if Graphiti is down, capture is lost. Tell the user to `indusk infra start`.
|
|
178
|
+
|
|
179
|
+
### Health Check
|
|
180
|
+
```
|
|
181
|
+
mcp__graphiti__get_status({})
|
|
182
|
+
```
|
|
183
|
+
Returns `{ status: "healthy", message: "..." }` when Graphiti is reachable and the database is connected.
|
package/hooks/check-gates.js
CHANGED
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* PreToolUse hook: blocks phase transitions in impl.md when gates are incomplete.
|
|
4
4
|
*
|
|
5
|
+
* The OTel gate is conditional on the project's `otel.role` in .indusk/config.json:
|
|
6
|
+
* - unset or "service": OTel gate is enforced (default)
|
|
7
|
+
* - "library" / "tool" / "none": OTel gate is silenced (mirrors validate-impl-structure)
|
|
8
|
+
*
|
|
5
9
|
* Exit 0 = allow the edit
|
|
6
10
|
* Exit 2 = block the edit (stderr sent to agent as feedback)
|
|
7
11
|
*/
|
|
8
12
|
|
|
9
|
-
import { readFileSync } from "node:fs";
|
|
13
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
14
|
+
import { resolve } from "node:path";
|
|
10
15
|
|
|
11
16
|
// Read hook input from stdin
|
|
12
17
|
let input = "";
|
|
@@ -115,14 +120,57 @@ function detectWorkflow(content) {
|
|
|
115
120
|
return m ? m[1] : "feature";
|
|
116
121
|
}
|
|
117
122
|
|
|
118
|
-
|
|
119
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Find the project root by walking up from a starting directory looking for
|
|
125
|
+
* a .indusk/ or .claude/ directory. Falls back to startDir if none found.
|
|
126
|
+
*/
|
|
127
|
+
function findProjectRoot(startDir) {
|
|
128
|
+
let dir = startDir;
|
|
129
|
+
for (let i = 0; i < 10; i++) {
|
|
130
|
+
if (existsSync(`${dir}/.indusk`) || existsSync(`${dir}/.claude`)) return dir;
|
|
131
|
+
const parent = resolve(dir, "..");
|
|
132
|
+
if (parent === dir) break;
|
|
133
|
+
dir = parent;
|
|
134
|
+
}
|
|
135
|
+
return startDir;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Whether the OTel gate should fire for this project. Reads .indusk/config.json
|
|
140
|
+
* and checks otel.role. Returns true if missing/unset/"service", false for
|
|
141
|
+
* library/tool/none. Mirrors shouldEmitOtelGate() in apps/indusk-mcp/src/lib/config.ts.
|
|
142
|
+
*/
|
|
143
|
+
function shouldEmitOtelGate(projectRoot) {
|
|
144
|
+
const configPath = `${projectRoot}/.indusk/config.json`;
|
|
145
|
+
if (!existsSync(configPath)) return true;
|
|
146
|
+
try {
|
|
147
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
148
|
+
const role = config?.otel?.role;
|
|
149
|
+
return role === undefined || role === "service";
|
|
150
|
+
} catch {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const projectRoot = findProjectRoot(event.cwd ?? process.cwd());
|
|
156
|
+
const otelGateEnabled = shouldEmitOtelGate(projectRoot);
|
|
157
|
+
|
|
158
|
+
// Which gate types are required per workflow.
|
|
159
|
+
// OTel is filtered out below when the project opts out via otel.role.
|
|
160
|
+
const WORKFLOW_GATES_BASE = {
|
|
120
161
|
feature: ["verification", "otel", "context", "document"],
|
|
121
162
|
refactor: ["verification", "otel", "context", "document"],
|
|
122
163
|
bugfix: ["verification", "document"],
|
|
123
164
|
spike: [],
|
|
124
165
|
};
|
|
125
166
|
|
|
167
|
+
const WORKFLOW_GATES = Object.fromEntries(
|
|
168
|
+
Object.entries(WORKFLOW_GATES_BASE).map(([wf, gates]) => [
|
|
169
|
+
wf,
|
|
170
|
+
otelGateEnabled ? gates : gates.filter((g) => g !== "otel"),
|
|
171
|
+
]),
|
|
172
|
+
);
|
|
173
|
+
|
|
126
174
|
// Parse phases from the NEW content (after edit) and OLD content (before edit)
|
|
127
175
|
function parsePhases(content) {
|
|
128
176
|
// Strip frontmatter
|
|
@@ -2,14 +2,20 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* PreToolUse hook: validates that impl phases have all four gate sections.
|
|
4
4
|
*
|
|
5
|
-
* Every phase must have: implementation items, Verification, OTel
|
|
5
|
+
* Every phase must have: implementation items, Verification, OTel*, Context, Document.
|
|
6
6
|
* Sections can opt out with (none needed), (not applicable), or skip-reason: {why}.
|
|
7
7
|
*
|
|
8
|
+
* *OTel section is conditional on the project's `otel.role` in .indusk/config.json:
|
|
9
|
+
* - unset or "service": OTel section is required (default behavior)
|
|
10
|
+
* - "library" / "tool" / "none": OTel section is NOT required (gate silenced)
|
|
11
|
+
* This mirrors `shouldEmitOtelGate()` in apps/indusk-mcp/src/lib/config.ts.
|
|
12
|
+
*
|
|
8
13
|
* Exit 0 = allow the edit
|
|
9
14
|
* Exit 2 = block the edit (stderr sent to agent as feedback)
|
|
10
15
|
*/
|
|
11
16
|
|
|
12
|
-
import { readFileSync } from "node:fs";
|
|
17
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
18
|
+
import { resolve } from "node:path";
|
|
13
19
|
|
|
14
20
|
// Read hook input from stdin
|
|
15
21
|
let input = "";
|
|
@@ -26,6 +32,43 @@ if (!filePath.endsWith("/impl.md") && !filePath.endsWith("\\impl.md")) {
|
|
|
26
32
|
process.exit(0);
|
|
27
33
|
}
|
|
28
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Find the project root by walking up from a starting directory looking for
|
|
37
|
+
* a .indusk/ or .claude/ directory. Falls back to event.cwd if none found.
|
|
38
|
+
* Mirrors the pattern used in check-catchup.js.
|
|
39
|
+
*/
|
|
40
|
+
function findProjectRoot(startDir) {
|
|
41
|
+
let dir = startDir;
|
|
42
|
+
for (let i = 0; i < 10; i++) {
|
|
43
|
+
if (existsSync(`${dir}/.indusk`) || existsSync(`${dir}/.claude`)) return dir;
|
|
44
|
+
const parent = resolve(dir, "..");
|
|
45
|
+
if (parent === dir) break;
|
|
46
|
+
dir = parent;
|
|
47
|
+
}
|
|
48
|
+
return startDir;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Whether the OTel gate should fire for this project. Reads .indusk/config.json
|
|
53
|
+
* and checks otel.role. Returns true if the config is missing, if otel.role is
|
|
54
|
+
* unset, or if otel.role is "service" — matches shouldEmitOtelGate() in
|
|
55
|
+
* apps/indusk-mcp/src/lib/config.ts exactly.
|
|
56
|
+
*/
|
|
57
|
+
function shouldEmitOtelGate(projectRoot) {
|
|
58
|
+
const configPath = `${projectRoot}/.indusk/config.json`;
|
|
59
|
+
if (!existsSync(configPath)) return true;
|
|
60
|
+
try {
|
|
61
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
62
|
+
const role = config?.otel?.role;
|
|
63
|
+
return role === undefined || role === "service";
|
|
64
|
+
} catch {
|
|
65
|
+
return true; // on parse error, preserve default behavior
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const projectRoot = findProjectRoot(event.cwd ?? process.cwd());
|
|
70
|
+
const otelGateEnabled = shouldEmitOtelGate(projectRoot);
|
|
71
|
+
|
|
29
72
|
// Check for skip-gates escape hatch
|
|
30
73
|
const newContent = toolInput.new_string ?? toolInput.content ?? "";
|
|
31
74
|
|
|
@@ -100,10 +143,13 @@ const body = fmMatch ? newFullContent.slice(fmMatch[0].length) : newFullContent;
|
|
|
100
143
|
const workflowMatch = frontmatter.match(/workflow:\s*(bugfix|refactor|feature|spike)/);
|
|
101
144
|
const workflow = workflowMatch ? workflowMatch[1] : "feature";
|
|
102
145
|
|
|
103
|
-
// Different workflows have different requirements
|
|
146
|
+
// Different workflows have different requirements.
|
|
147
|
+
// OTel is further gated on the project's `otel.role` in .indusk/config.json —
|
|
148
|
+
// libraries/tools/none opt out of the OTel gate entirely. Workflows that normally
|
|
149
|
+
// require OTel (feature, refactor) will only require it when otelGateEnabled is true.
|
|
104
150
|
const requirements = {
|
|
105
|
-
feature: { verification: true, otel:
|
|
106
|
-
refactor: { verification: true, otel:
|
|
151
|
+
feature: { verification: true, otel: otelGateEnabled, context: true, document: true },
|
|
152
|
+
refactor: { verification: true, otel: otelGateEnabled, context: true, document: true },
|
|
107
153
|
bugfix: { verification: true, otel: false, context: false, document: true },
|
|
108
154
|
spike: { verification: false, otel: false, context: false, document: false },
|
|
109
155
|
}[workflow];
|
package/package.json
CHANGED
package/skills/catchup.md
CHANGED
|
@@ -64,6 +64,29 @@ Read it fully. Don't skim.
|
|
|
64
64
|
|
|
65
65
|
**After reading, edit the handoff to check off:** `- [x] context`
|
|
66
66
|
|
|
67
|
+
### 4.5. Recall from Graphiti
|
|
68
|
+
|
|
69
|
+
CLAUDE.md is the stable, slow-changing layer of project memory. Graphiti is the fast, temporal layer — it captures decisions, corrections, and retrospective insights as they happen. Catchup pulls both layers so the agent starts the session with full context.
|
|
70
|
+
|
|
71
|
+
**Recall recent decisions and lessons:**
|
|
72
|
+
```
|
|
73
|
+
mcp__graphiti__search_nodes({
|
|
74
|
+
query: "recent decisions and lessons",
|
|
75
|
+
group_ids: ["{project-group}", "shared"],
|
|
76
|
+
max_nodes: 8
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The project group comes from the `getProjectGroupId(projectRoot)` helper (in `apps/indusk-mcp/src/lib/config.ts`). Always include both the project group and `shared` so cross-project conventions surface alongside project-specific knowledge.
|
|
81
|
+
|
|
82
|
+
**Surface contradictions:** look at the returned nodes for any whose `attributes` reference recently invalidated facts (Graphiti marks superseded facts with `invalid_at`). If a recently invalidated fact relates to an active plan or current code area, flag it to the user — those are places where assumptions changed.
|
|
83
|
+
|
|
84
|
+
**Output format:** include a "Graphiti recall" section in the catchup summary with the most relevant 3-5 nodes by name + summary. Don't dump everything — surface what's actionable.
|
|
85
|
+
|
|
86
|
+
**Graceful degradation:** If `mcp__graphiti__search_nodes` is unavailable (Graphiti container down, transport error), skip this step silently and add a note to the catchup summary: `Graphiti: unavailable (run \`indusk infra start\` to recall episodic memory)`. Catchup should not fail if Graphiti is down — the rest of the layers are still valid.
|
|
87
|
+
|
|
88
|
+
**After completing recall, edit the handoff to check off:** `- [x] graphiti` (added to the catchup status box section)
|
|
89
|
+
|
|
67
90
|
### 5. Check Active Plans
|
|
68
91
|
Call `list_plans`. This shows every plan and its status. Pay attention to:
|
|
69
92
|
- Plans with status `in-progress` — these are actively being worked on
|
|
@@ -104,6 +127,7 @@ After completing all steps, present a brief summary to the user:
|
|
|
104
127
|
- Extensions: N enabled [list names]
|
|
105
128
|
- Active plans: [list with current phase]
|
|
106
129
|
- Codebase: [N files indexed]
|
|
130
|
+
- Graphiti recall: [3-5 most relevant nodes by name + summary, or "unavailable" if Graphiti is down]
|
|
107
131
|
|
|
108
132
|
Ready to pick up. What would you like to do?
|
|
109
133
|
```
|
package/skills/planner.md
CHANGED
|
@@ -74,8 +74,32 @@ Workflow templates are in `templates/workflows/` in the package. They describe w
|
|
|
74
74
|
|
|
75
75
|
4. **If research is done**, write the brief. This is where a direction emerges from the research. The brief proposes what we're building and why, informed by what the research uncovered. **Consider creating a visual sketch** of the proposed architecture with Excalidraw (if the extension is enabled) — a hand-drawn diagram makes the proposal concrete and easier to discuss. **Present the brief and have a conversation about it.** Don't just ask "does this look good?" — walk the user through it: "Here's what I'm proposing we build. Does this match what you had in mind? Is there anything missing, or anything here you don't want?" Iterate until the user is genuinely happy with the direction, then mark it as `accepted`.
|
|
76
76
|
|
|
77
|
+
**When the brief moves from `draft` to `accepted`**, capture the decision in Graphiti:
|
|
78
|
+
```
|
|
79
|
+
mcp__graphiti__add_memory({
|
|
80
|
+
name: "brief-accepted-{plan-name}",
|
|
81
|
+
episode_body: "{Problem}\n\n{Proposed Direction}\n\n{Scope summary if helpful}",
|
|
82
|
+
group_id: "{project-group}",
|
|
83
|
+
source: "text",
|
|
84
|
+
source_description: "brief acceptance"
|
|
85
|
+
})
|
|
86
|
+
```
|
|
87
|
+
The `group_id` is the project group (use `getProjectGroupId(projectRoot)` from `apps/indusk-mcp/src/lib/config.ts` — defaults to project directory basename, override via `.indusk/config.json` `graphiti.groupId`). Skip silently if `mcp__graphiti__add_memory` is unavailable (Graphiti may be down — degrade gracefully, do not fail the brief acceptance).
|
|
88
|
+
|
|
77
89
|
5. **If brief is accepted** and the workflow includes an ADR (feature only), write the ADR. The ADR formalizes the decisions that were discussed during research and led to the brief. It records what was chosen, what was rejected, and why. **After the ADR is accepted**, add a one-liner to CLAUDE.md's Key Decisions section per the context skill: `- {decision summary} — see .indusk/planning/{plan}/adr.md`
|
|
78
90
|
|
|
91
|
+
**When the ADR moves from `proposed` to `accepted`**, capture the Y-statement in Graphiti:
|
|
92
|
+
```
|
|
93
|
+
mcp__graphiti__add_memory({
|
|
94
|
+
name: "adr-{plan-name}",
|
|
95
|
+
episode_body: "In the context of {use case}, facing {constraint}, we decided for {chosen option} and against {rejected alternatives}, to achieve {desired outcome}, accepting {tradeoff}, because {rationale}.",
|
|
96
|
+
group_id: "{project-group}",
|
|
97
|
+
source: "text",
|
|
98
|
+
source_description: "ADR acceptance"
|
|
99
|
+
})
|
|
100
|
+
```
|
|
101
|
+
The Y-statement is rich enough that Graphiti will extract entities for the chosen option, rejected alternatives, constraint, and rationale — and will detect contradictions if a later ADR overrides this one. Skip silently on Graphiti unavailability.
|
|
102
|
+
|
|
79
103
|
6. **If ADR is accepted** (or brief is accepted for bugfix/refactor), write the impl. Break into phased checklists with concrete tasks. For refactor workflows, include a `## Boundary Map` section. For multi-phase impls of any type, consider adding a boundary map.
|
|
80
104
|
|
|
81
105
|
**Gate policy applies when writing impls.** Set `gate_policy` in the impl frontmatter (`strict`, `ask`, or `auto`). The `validate-impl-structure` hook enforces this at write time:
|
|
@@ -84,6 +108,8 @@ Workflow templates are in `templates/workflows/` in the package. They describe w
|
|
|
84
108
|
|
|
85
109
|
Default is `ask`. See the work skill "Gate Override Policy" for full details on what each mode enforces at execution time.
|
|
86
110
|
|
|
111
|
+
**OTel gate is conditional on `otel.role`.** Read `.indusk/config.json` for the project's `otel.role` field (or use the `shouldEmitOtelGate(projectRoot)` helper from `apps/indusk-mcp/src/lib/config.ts`). The OTel gate fires for projects whose `otel.role` is unset or `"service"` — these are user-facing apps that produce telemetry you want to collect. **Do NOT write `#### Phase N OTel` sections** for projects whose `otel.role` is `"library"`, `"tool"`, or `"none"` — these are libraries, CLIs, or scripts that should never emit telemetry and writing OTel gates for them is friction without value. The `validate-impl-structure` and `check-gates` hooks apply the same rule. The other gates (verify, context, document) always apply regardless of `otel.role`.
|
|
112
|
+
|
|
87
113
|
7. **If impl is completed** (all items checked off by `/work`), invoke the retrospective skill (`/retrospective {plan-name}`). This handles the structured audit (docs, tests, quality, context), knowledge handoff to the docs site, and archival. Do not write a freeform retrospective — use the skill. (Bugfix and refactor workflows may skip retrospective for small changes — user's call.)
|
|
88
114
|
|
|
89
115
|
8. **Always present each document for review** before moving to the next stage. The user signs off on each step.
|
|
@@ -256,6 +282,8 @@ For multi-phase impls, include a boundary map showing what each phase produces a
|
|
|
256
282
|
function withdrawFor(wallet: address, player: address, amount: uint256, historyHash: bytes32)
|
|
257
283
|
```
|
|
258
284
|
|
|
285
|
+
{OPTIONAL: #### Phase 1 OTel — include ONLY if the project's `otel.role` in `.indusk/config.json` is unset or `"service"`. Skip the entire OTel block for projects with `otel.role: "library" | "tool" | "none"`. Use `shouldEmitOtelGate(projectRoot)` from `apps/indusk-mcp/src/lib/config.ts` to decide.}
|
|
286
|
+
|
|
259
287
|
#### Phase 1 OTel
|
|
260
288
|
- [ ] {Instrumentation check — are new code paths observable? See the OTel skill for patterns. Example items: "New endpoints have manual spans with `otel.category` and domain attributes", "Errors recorded with `recordException` + `setStatus(ERROR)` + trace-correlated log". Ask: "did this phase add endpoints, business logic, state transitions, or error paths?" If not, this section can be opted out per gate policy.}
|
|
261
289
|
|
package/skills/retrospective.md
CHANGED
|
@@ -91,6 +91,36 @@ If yes, call `add_lesson` for each one. These become personal lessons in `.claud
|
|
|
91
91
|
|
|
92
92
|
If no lessons emerged, that's fine — not every plan produces new knowledge. Move on.
|
|
93
93
|
|
|
94
|
+
**Also capture each retrospective insight in Graphiti** so it becomes part of the temporal knowledge graph and can surface in future searches and contradictions.
|
|
95
|
+
|
|
96
|
+
For each item in the retrospective's **What We Learned** section:
|
|
97
|
+
```
|
|
98
|
+
mcp__graphiti__add_memory({
|
|
99
|
+
name: "retro-{plan-name}-{n}",
|
|
100
|
+
episode_body: "{the full insight, including context — not just a one-line summary}",
|
|
101
|
+
group_id: "{project-group}",
|
|
102
|
+
source: "text",
|
|
103
|
+
source_description: "retrospective lesson"
|
|
104
|
+
})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
For each item in the retrospective's **What We'd Do Differently** section:
|
|
108
|
+
```
|
|
109
|
+
mcp__graphiti__add_memory({
|
|
110
|
+
name: "retro-{plan-name}-wdid-{n}",
|
|
111
|
+
episode_body: "{the hindsight item, with reasoning}",
|
|
112
|
+
group_id: "{project-group}",
|
|
113
|
+
source: "text",
|
|
114
|
+
source_description: "retrospective hindsight"
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Use the project group (from `getProjectGroupId(projectRoot)`) — most retrospective insights are project-specific. Promote to `shared` only if the insight is clearly cross-project (those should also become a personal lesson via `add_lesson`).
|
|
119
|
+
|
|
120
|
+
**Contradictions:** If the retrospective surfaces a moment where "we thought X but found Y", capture both as separate episodes. Graphiti's contradiction detection will invalidate the older fact when it sees the conflicting one. This is one of Graphiti's most useful features — it remembers that a previous assumption was overturned, so the agent doesn't accidentally re-introduce it later.
|
|
121
|
+
|
|
122
|
+
Skip silently if `mcp__graphiti__add_memory` is unavailable — Graphiti capture is best-effort, and lesson recording via `add_lesson` is the canonical path. Graphiti capture is supplementary.
|
|
123
|
+
|
|
94
124
|
### Step 7: Context Audit
|
|
95
125
|
|
|
96
126
|
Re-read CLAUDE.md in full. After the entire impl is done, verify:
|
package/skills/work.md
CHANGED
|
@@ -202,6 +202,27 @@ When you are corrected mid-work — the user says "no, not that way" or "don't d
|
|
|
202
202
|
|
|
203
203
|
Don't wait to be told. Corrections are the most valuable source of project knowledge.
|
|
204
204
|
|
|
205
|
+
**When the user confirms `context learn`, ALSO capture the correction in Graphiti:**
|
|
206
|
+
```
|
|
207
|
+
mcp__graphiti__add_memory({
|
|
208
|
+
name: "correction-{slug}",
|
|
209
|
+
episode_body: "{lesson text}",
|
|
210
|
+
group_id: "{shared OR project-group}",
|
|
211
|
+
source: "text",
|
|
212
|
+
source_description: "user correction"
|
|
213
|
+
})
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Where `{slug}` is a short kebab-case label for the topic (e.g. `correction-pnpm-ce`, `correction-graphiti-error-handling`).
|
|
217
|
+
|
|
218
|
+
**Choosing `shared` vs project group:**
|
|
219
|
+
- **`shared`**: tools, conventions, patterns that apply across projects. Examples: "always use pnpm ce", "never mock the database in integration tests", "use jj describe-then-do for commits". The lesson is generally true and a different project would benefit from it.
|
|
220
|
+
- **`{project-group}`**: facts specific to this project's code, data, or domain. Examples: "the impl-parser handles four gate types per phase", "graph_ensure auto-repairs the indusk-infra container". The lesson only makes sense in the context of this project.
|
|
221
|
+
|
|
222
|
+
When in doubt, ask: "Would this correction make sense to a different project?" Yes → `shared`. No → project group.
|
|
223
|
+
|
|
224
|
+
Use `getProjectGroupId(projectRoot)` from `apps/indusk-mcp/src/lib/config.ts` to get the project group consistently. Skip silently if `mcp__graphiti__add_memory` is unavailable — Graphiti capture is best-effort, do not fail the work item.
|
|
225
|
+
|
|
205
226
|
## Commits (jj)
|
|
206
227
|
|
|
207
228
|
Use the **describe-then-do** workflow from the jj skill:
|