@infinitedusky/indusk-mcp 1.9.2 → 1.10.0

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.
@@ -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 { disableExtension, disabledDir, enableExtension, ensureExtensionsDirs, extensionConfigDir, extensionsDir, getEnabledExtensions, isEnabled, loadExtension, loadExtensions, resolveManifestPath, } from "../../lib/extension-loader.js";
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", "add", "-t", "http",
639
- ...headerArgs.flatMap(h => {
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", "project", "--", name, url,
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}`, { cwd: process.cwd(), timeout: 10000, stdio: ["ignore", "pipe", "pipe"] });
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
@@ -46,6 +46,9 @@ function createCgcIgnore(projectRoot) {
46
46
  "dist/",
47
47
  "build/",
48
48
  ".git/",
49
+ ".jj/",
50
+ ".indusk/",
51
+ ".claude/",
49
52
  "*.png",
50
53
  "*.jpg",
51
54
  "*.svg",
@@ -296,6 +299,21 @@ export async function init(projectRoot, options = {}) {
296
299
  else {
297
300
  console.info(" skip: codegraphcontext MCP server (already exists)");
298
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
+ }
299
317
  // 4b. Check infrastructure container
300
318
  console.info("\n[Infrastructure]");
301
319
  try {
@@ -755,12 +773,52 @@ export async function init(projectRoot, options = {}) {
755
773
  writeConfig(projectRoot, config);
756
774
  console.info(`\n[Config]`);
757
775
  console.info(` create: .indusk/config.json (mode: ${config.mode})`);
776
+ // Create initial handoff so /catchup runs full orientation on first session
777
+ const handoffPath = join(projectRoot, ".claude/handoff.md");
778
+ if (!existsSync(handoffPath) || force) {
779
+ const today = new Date().toISOString().split("T")[0];
780
+ const handoffContent = [
781
+ "# Handoff",
782
+ "",
783
+ `**Date:** ${today}`,
784
+ "**Session:** Fresh init — first session orientation needed",
785
+ "",
786
+ "## Catchup Status",
787
+ "- [ ] mcp-ready",
788
+ "- [ ] handoff",
789
+ "- [ ] lessons",
790
+ "- [ ] health",
791
+ "- [ ] context",
792
+ "- [ ] plans",
793
+ "- [ ] skills",
794
+ "- [ ] extensions",
795
+ "- [ ] graph",
796
+ "",
797
+ "## What Was Being Worked On",
798
+ "`indusk init` just set up this project. No prior session.",
799
+ "",
800
+ "## Where It Stopped",
801
+ "Init complete. Agent needs full orientation (lessons, context, skills, extensions, graph).",
802
+ "",
803
+ "## What's Next",
804
+ "1. Run `/catchup` to orient the agent (reads lessons, context, skills, extensions, graph)",
805
+ "2. Edit CLAUDE.md with project details",
806
+ "3. Start planning: `/planner your-first-feature`",
807
+ "",
808
+ "## Open Issues",
809
+ "None — fresh project.",
810
+ "",
811
+ ].join("\n");
812
+ writeFileSync(handoffPath, handoffContent);
813
+ console.info("\n[Handoff]");
814
+ console.info(" create: .claude/handoff.md (first-session orientation)");
815
+ }
758
816
  // Summary
759
817
  console.info("\nDone!");
760
818
  console.info("\n⚠ Restart Claude Code to load the updated MCP server and skills.");
761
819
  console.info("\nNext steps:");
762
820
  console.info(" 1. Set up infrastructure (if not done): indusk infra start");
763
- console.info(" 2. Restart Claude Code");
821
+ console.info(" 2. Restart Claude Code — /catchup will auto-orient the agent");
764
822
  console.info(" 3. Edit CLAUDE.md with your project details");
765
- console.info(" 4. Start planning: /plan your-first-feature");
823
+ console.info(" 4. Start planning: /planner your-first-feature");
766
824
  }
@@ -274,7 +274,38 @@ export async function update(projectRoot) {
274
274
  console.info(` ${name}: update hook failed`);
275
275
  }
276
276
  }
277
- if (manifest?.mcp_server?.setup_instructions) {
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
  }
@@ -14,8 +14,27 @@ 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
+ };
17
25
  }
18
26
  export declare function getConfigPath(projectRoot: string): string;
19
27
  export declare function readConfig(projectRoot: string): InduskConfig | null;
20
28
  export declare function writeConfig(projectRoot: string, config: InduskConfig): void;
21
29
  export declare function getPlanningDir(projectRoot: string): string;
30
+ /**
31
+ * Get the Graphiti group id for project-specific episodes.
32
+ *
33
+ * Resolution order:
34
+ * 1. .indusk/config.json `graphiti.groupId` if set
35
+ * 2. Project directory basename
36
+ *
37
+ * Use `[getProjectGroupId(root), "shared"]` as the default group_ids list when
38
+ * searching Graphiti — this gives both project-scoped and cross-project knowledge.
39
+ */
40
+ export declare function getProjectGroupId(projectRoot: string): string;
@@ -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,19 @@ 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
+ }
@@ -53,6 +53,7 @@ export interface ExtensionManifest {
53
53
  headers?: Record<string, string>;
54
54
  env?: Record<string, string>;
55
55
  env_from_shell?: string[];
56
+ add_command?: string;
56
57
  setup_instructions?: string[];
57
58
  };
58
59
  }
@@ -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
  }
@@ -41,7 +41,7 @@ That's it. No tokens, no config files, no environment variables.
41
41
 
42
42
  ## When to Create Diagrams
43
43
 
44
- - **During `/plan` research or brief**: sketch the proposed architecture to make it concrete
44
+ - **During `/planner` research or brief**: sketch the proposed architecture to make it concrete
45
45
  - **During `/work` teach mode**: visualize what a phase is building before and after
46
46
  - **During debugging**: draw the request flow to identify where things break
47
47
  - **During retrospective**: before/after architecture comparison
@@ -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
- - **Episode capture**: After a decision, retro finding, or correction — anything worth remembering across sessions
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
- - **Context retrieval**: At session start or when working in an unfamiliar area
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. The `GraphitiClient` does this automatically.
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
- After an ADR is accepted or a significant choice is made:
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
- addEpisode("auth-approach-decision",
39
- "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.",
40
- { groupId: "myproject" })
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
- When someone corrects the agent or a mistake is found:
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
- addEpisode("correction-test-database",
47
- "Integration tests must use a real database, not mocks. We got burned when mocked tests passed but the production migration failed.",
48
- { groupId: "shared" })
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
- searchNodes("authentication middleware")
55
- searchFacts("how does auth work in this project")
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
- After a plan retrospective surfaces a useful insight:
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
- addEpisode("retro-gate-enforcement",
62
- "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.",
63
- { groupId: "myproject" })
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 (CGC handles this)
69
- - Git history (git log handles this)
70
- - Ephemeral state (current task, in-progress work)
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
- - CGC graph tools still check FalkorDB directly
90
- - Graphiti client methods return null/empty (never throw)
91
- - The agent continues working with flat-file context (CLAUDE.md, lessons, skills)
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infinitedusky/indusk-mcp",
3
- "version": "1.9.2",
3
+ "version": "1.10.0",
4
4
  "description": "InDusk development system — skills, MCP tools, and CLI for structured AI-assisted development",
5
5
  "type": "module",
6
6
  "files": [
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
@@ -78,7 +101,7 @@ Call `extensions_status` to see what extensions are enabled and their capabiliti
78
101
  Call `get_skill_summaries` to load the name, description, and type of every installed skill. This returns a compact summary — you do NOT need to read each skill file individually. The full skill content loads automatically when the user invokes a slash command.
79
102
 
80
103
  Skill types:
81
- - **process** — workflow skills with slash commands (plan, work, verify, context, document, retrospective)
104
+ - **process** — workflow skills with slash commands (planner, work, verify, context, document, retrospective)
82
105
  - **extension** — tool integrations (cgc, composable-env, excalidraw, etc.)
83
106
  - **domain** — technology-specific best practices (typescript, testing, etc.)
84
107
 
@@ -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
  ```
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: plan
2
+ name: planner
3
3
  description: Create and advance plans. Every plan follows the same document lifecycle — research, brief, ADR, impl, retrospective. Knows how to write each one, what order they go in, and how to pick up where things left off.
4
4
  argument-hint: "[workflow] [plan name] — workflow: feature (default), bugfix, refactor, spike"
5
5
  ---
@@ -29,15 +29,15 @@ General-purpose research (insights useful across plans) also lives in `research/
29
29
 
30
30
  ## Workflow Types
31
31
 
32
- The first argument to `/plan` can optionally be a workflow type that controls which documents are created:
32
+ The first argument to `/planner` can optionally be a workflow type that controls which documents are created:
33
33
 
34
34
  | Command | Workflow | Documents |
35
35
  |---------|----------|-----------|
36
- | `/plan bugfix auth-expiry` | bugfix | brief + impl only |
37
- | `/plan refactor extract-auth` | refactor | brief + impl (with boundary map) |
38
- | `/plan spike redis-options` | spike | research only |
39
- | `/plan feature payment-flow` | feature | full lifecycle (default) |
40
- | `/plan payment-flow` | feature | same — no type defaults to feature |
36
+ | `/planner bugfix auth-expiry` | bugfix | brief + impl only |
37
+ | `/planner refactor extract-auth` | refactor | brief + impl (with boundary map) |
38
+ | `/planner spike redis-options` | spike | research only |
39
+ | `/planner feature payment-flow` | feature | full lifecycle (default) |
40
+ | `/planner payment-flow` | feature | same — no type defaults to feature |
41
41
 
42
42
  Parse the input: if the first word is `bugfix`, `refactor`, `spike`, or `feature`, use that workflow. Otherwise, default to `feature`. The remaining words become the plan name (kebab-cased).
43
43
 
@@ -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:
@@ -21,7 +21,7 @@ The retrospective skill replaces the freeform "write a retrospective" step with
21
21
  ## When to Use
22
22
 
23
23
  - After `/work` completes all impl phases and the status is `completed`
24
- - When `/plan {name}` detects the impl is completed and the next step is retrospective
24
+ - When `/planner {name}` detects the impl is completed and the next step is retrospective
25
25
  - Directly via `/retrospective {plan-name}`
26
26
 
27
27
  ## The Audit Checklist
@@ -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:
@@ -17,7 +17,7 @@ To see which MCP servers and extensions are active, call `extensions_status`.
17
17
  ## How the Skills Work Together
18
18
 
19
19
  ```
20
- /plan → creates planning docs (research, brief, ADR, impl)
20
+ /planner → creates planning docs (research, brief, ADR, impl)
21
21
 
22
22
  /work → executes impl checklist, phase by phase
23
23
  each phase has four gates:
@@ -139,7 +139,7 @@ While executing impl items:
139
139
 
140
140
  Gates prevent skipping important work. Three enforcement levels, set via `gate_policy` in impl frontmatter or `.claude/settings.json`:
141
141
 
142
- | Mode | Writing the impl (`/plan`) | Executing the impl (`/work`) |
142
+ | Mode | Writing the impl (`/planner`) | Executing the impl (`/work`) |
143
143
  |------|---------------------------|------------------------------|
144
144
  | **`strict`** | Every gate must have a real item. No `(none needed)`. | Every item must be completed. No skipping. |
145
145
  | **`ask`** (default) | Every gate must have a real item. No `(none needed)`. | Skip only with conversation proof: `(none needed — asked: "..." — user: "...")` |
package/skills/work.md CHANGED
@@ -70,7 +70,7 @@ Three modes, configured via `gate_policy` in the impl frontmatter or `.claude/se
70
70
 
71
71
  | Mode | Behavior |
72
72
  |------|----------|
73
- | **`strict`** | No overrides at any stage. Every gate must have a real item when the impl is written (`/plan`), and every item must be completed during `/work`. No `(none needed)`, no `skip-reason:`, no conversation proof. |
73
+ | **`strict`** | No overrides at any stage. Every gate must have a real item when the impl is written (`/planner`), and every item must be completed during `/work`. No `(none needed)`, no `skip-reason:`, no conversation proof. |
74
74
  | **`ask`** (default) | Every gate must have a real item when the impl is written. During `/work`, the agent must ask the user before skipping, and include proof of the conversation in the skip format. Hooks enforce both stages. |
75
75
  | **`auto`** | Gates can be pre-filled with `(none needed)` or `skip-reason:` at write time. During `/work`, the agent can skip without asking. Use when running autonomously. |
76
76
 
@@ -118,7 +118,7 @@ In `ask` mode, skipped gates MUST include proof that the conversation happened:
118
118
 
119
119
  The hook validates that both `asked:` and `user:` are present with non-empty quoted content. Bare `(none needed)` or `skip-reason:` without conversation proof will be **blocked by the hook**.
120
120
 
121
- | Mode | At write time (`/plan`) | At execution time (`/work`) |
121
+ | Mode | At write time (`/planner`) | At execution time (`/work`) |
122
122
  |------|------------------------|---------------------------|
123
123
  | `strict` | No opt-outs — real items required | No skipping — everything completed |
124
124
  | `ask` | No opt-outs — real items required | Skip only with conversation proof |
@@ -148,7 +148,7 @@ The hook validates that both `asked:` and `user:` are present with non-empty quo
148
148
  - Update impl status to `completed`
149
149
  - Summarize what was done
150
150
  - If this plan included an ADR, confirm CLAUDE.md's Key Decisions was updated
151
- - Let the user know the plan is ready for a retrospective if they want one (`/plan {name}` will pick up at the retrospective stage)
151
+ - Let the user know the plan is ready for a retrospective if they want one (`/planner {name}` will pick up at the retrospective stage)
152
152
 
153
153
  ## Teach Mode
154
154
 
@@ -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: