@infinitedusky/indusk-mcp 1.7.0 → 1.7.3

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.
@@ -215,6 +215,45 @@ export async function init(projectRoot, options = {}) {
215
215
  else {
216
216
  console.info(" skip: codegraphcontext MCP server (already exists)");
217
217
  }
218
+ // 4b. Check infrastructure container
219
+ console.info("\n[Infrastructure]");
220
+ try {
221
+ const infraStatus = execSync("docker inspect --format='{{.State.Running}}' indusk-infra", {
222
+ encoding: "utf-8",
223
+ timeout: 5000,
224
+ stdio: ["ignore", "pipe", "pipe"],
225
+ }).trim();
226
+ if (infraStatus === "true") {
227
+ console.info(" ok: indusk-infra container is running");
228
+ }
229
+ else {
230
+ console.info(" starting: indusk-infra container...");
231
+ execSync("docker start indusk-infra", {
232
+ timeout: 15000,
233
+ stdio: ["ignore", "pipe", "pipe"],
234
+ });
235
+ console.info(" started: indusk-infra");
236
+ }
237
+ }
238
+ catch {
239
+ console.info(" skip: indusk-infra container not found");
240
+ console.info(" To set up infrastructure: indusk infra start");
241
+ }
242
+ // 4c. Copy Graphiti extension manifest
243
+ const graphitiExtDir = join(projectRoot, ".indusk/extensions/graphiti");
244
+ const graphitiManifest = join(graphitiExtDir, "manifest.json");
245
+ if (existsSync(graphitiManifest) && !force) {
246
+ console.info(" skip: graphiti extension (already exists)");
247
+ }
248
+ else {
249
+ mkdirSync(graphitiExtDir, { recursive: true });
250
+ cpSync(join(packageRoot, "extensions/graphiti/manifest.json"), graphitiManifest);
251
+ const skillSource = join(packageRoot, "extensions/graphiti/skill.md");
252
+ if (existsSync(skillSource)) {
253
+ cpSync(skillSource, join(graphitiExtDir, "skill.md"));
254
+ }
255
+ console.info(" create: .indusk/extensions/graphiti/ (manifest + skill)");
256
+ }
218
257
  // 5. Generate .vscode/settings.json
219
258
  console.info("\n[Editor]");
220
259
  const vscodePath = join(projectRoot, ".vscode/settings.json");
@@ -568,7 +607,8 @@ export async function init(projectRoot, options = {}) {
568
607
  console.info("\nDone!");
569
608
  console.info("\n⚠ Restart Claude Code to load the updated MCP server and skills.");
570
609
  console.info("\nNext steps:");
571
- console.info(" 1. Restart Claude Code");
572
- console.info(" 2. Edit CLAUDE.md with your project details");
573
- console.info(" 3. Start planning: /plan your-first-feature");
610
+ console.info(" 1. Set up infrastructure (if not done): indusk infra start");
611
+ console.info(" 2. Restart Claude Code");
612
+ console.info(" 3. Edit CLAUDE.md with your project details");
613
+ console.info(" 4. Start planning: /plan your-first-feature");
574
614
  }
@@ -0,0 +1,93 @@
1
+ interface AddEpisodeOptions {
2
+ groupId?: string;
3
+ source?: string;
4
+ sourceDescription?: string;
5
+ }
6
+ interface SearchOptions {
7
+ groupIds?: string[];
8
+ maxResults?: number;
9
+ }
10
+ interface GraphitiNode {
11
+ name: string;
12
+ uuid: string;
13
+ summary: string;
14
+ group_id: string;
15
+ [key: string]: unknown;
16
+ }
17
+ interface GraphitiFact {
18
+ uuid: string;
19
+ fact: string;
20
+ valid_at: string | null;
21
+ invalid_at: string | null;
22
+ [key: string]: unknown;
23
+ }
24
+ /**
25
+ * MCP client wrapper for the Graphiti temporal knowledge graph.
26
+ *
27
+ * Connects to the Graphiti MCP server running inside the indusk-infra container.
28
+ * Lazy connection — connects on first call, not at construction.
29
+ * Graceful degradation — returns null/empty on connection failure, never throws.
30
+ */
31
+ export declare class GraphitiClient {
32
+ private client;
33
+ private transport;
34
+ private connected;
35
+ private connecting;
36
+ private serverUrl;
37
+ private projectName;
38
+ constructor(projectName: string, serverUrl?: string);
39
+ /**
40
+ * Lazily connect to the Graphiti MCP server.
41
+ * Returns true if connected, false if connection failed.
42
+ * Multiple concurrent calls share the same connection attempt.
43
+ */
44
+ connect(): Promise<boolean>;
45
+ private doConnect;
46
+ private getClient;
47
+ /**
48
+ * Add an episode to the knowledge graph.
49
+ *
50
+ * @param name - Short name for the episode (e.g., "auth-refactor decision")
51
+ * @param body - Episode content to extract entities and facts from
52
+ * @param options - groupId defaults to project name, source defaults to "text"
53
+ * @returns Success response or null on failure
54
+ */
55
+ addEpisode(name: string, body: string, options?: AddEpisodeOptions): Promise<{
56
+ success: boolean;
57
+ } | null>;
58
+ /**
59
+ * Search for entity nodes in the knowledge graph.
60
+ *
61
+ * @param query - Natural language search query
62
+ * @param options - groupIds defaults to [projectName, "shared"] for cross-group search
63
+ * @returns Array of matching nodes, or empty array on failure
64
+ */
65
+ searchNodes(query: string, options?: SearchOptions): Promise<GraphitiNode[]>;
66
+ /**
67
+ * Search for facts (relationships between entities) in the knowledge graph.
68
+ *
69
+ * @param query - Natural language search query
70
+ * @param options - groupIds defaults to [projectName, "shared"] for cross-group search
71
+ * @returns Array of matching facts, or empty array on failure
72
+ */
73
+ searchFacts(query: string, options?: SearchOptions): Promise<GraphitiFact[]>;
74
+ /**
75
+ * Check if the Graphiti server is reachable and healthy.
76
+ *
77
+ * @returns Status object or null if unreachable
78
+ */
79
+ getStatus(): Promise<{
80
+ status: string;
81
+ } | null>;
82
+ /**
83
+ * Resolve group IDs for search — always includes "shared" for cross-group search.
84
+ * If caller provides groupIds, uses those. Otherwise defaults to [projectName, "shared"].
85
+ * Deduplicates "shared" if already present.
86
+ */
87
+ private resolveGroupIds;
88
+ /**
89
+ * Disconnect from the Graphiti MCP server.
90
+ */
91
+ disconnect(): Promise<void>;
92
+ }
93
+ export {};
@@ -0,0 +1,209 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
3
+ import { getInfraConfig } from "./infra-config.js";
4
+ /**
5
+ * MCP client wrapper for the Graphiti temporal knowledge graph.
6
+ *
7
+ * Connects to the Graphiti MCP server running inside the indusk-infra container.
8
+ * Lazy connection — connects on first call, not at construction.
9
+ * Graceful degradation — returns null/empty on connection failure, never throws.
10
+ */
11
+ export class GraphitiClient {
12
+ client = null;
13
+ transport = null;
14
+ connected = false;
15
+ connecting = null;
16
+ serverUrl;
17
+ projectName;
18
+ constructor(projectName, serverUrl) {
19
+ this.projectName = projectName;
20
+ this.serverUrl = serverUrl ?? getInfraConfig().graphiti.url;
21
+ }
22
+ /**
23
+ * Lazily connect to the Graphiti MCP server.
24
+ * Returns true if connected, false if connection failed.
25
+ * Multiple concurrent calls share the same connection attempt.
26
+ */
27
+ async connect() {
28
+ if (this.connected)
29
+ return true;
30
+ if (this.connecting)
31
+ return this.connecting;
32
+ this.connecting = this.doConnect();
33
+ const result = await this.connecting;
34
+ this.connecting = null;
35
+ return result;
36
+ }
37
+ async doConnect() {
38
+ try {
39
+ // Normalize URL — Graphiti redirects /mcp/ to /mcp
40
+ const url = this.serverUrl.endsWith("/") ? this.serverUrl.slice(0, -1) : this.serverUrl;
41
+ this.transport = new StreamableHTTPClientTransport(new URL(url));
42
+ this.client = new Client({ name: "indusk-mcp", version: "1.0.0" });
43
+ await this.client.connect(this.transport);
44
+ this.connected = true;
45
+ return true;
46
+ }
47
+ catch {
48
+ this.client = null;
49
+ this.transport = null;
50
+ this.connected = false;
51
+ return false;
52
+ }
53
+ }
54
+ getClient() {
55
+ if (!this.client)
56
+ throw new Error("Not connected");
57
+ return this.client;
58
+ }
59
+ /**
60
+ * Add an episode to the knowledge graph.
61
+ *
62
+ * @param name - Short name for the episode (e.g., "auth-refactor decision")
63
+ * @param body - Episode content to extract entities and facts from
64
+ * @param options - groupId defaults to project name, source defaults to "text"
65
+ * @returns Success response or null on failure
66
+ */
67
+ async addEpisode(name, body, options) {
68
+ if (!(await this.connect()))
69
+ return null;
70
+ try {
71
+ const result = await this.getClient().callTool({
72
+ name: "add_memory",
73
+ arguments: {
74
+ name,
75
+ episode_body: body,
76
+ group_id: options?.groupId ?? this.projectName,
77
+ source: options?.source ?? "text",
78
+ source_description: options?.sourceDescription ?? "",
79
+ },
80
+ });
81
+ return { success: !result.isError };
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ /**
88
+ * Search for entity nodes in the knowledge graph.
89
+ *
90
+ * @param query - Natural language search query
91
+ * @param options - groupIds defaults to [projectName, "shared"] for cross-group search
92
+ * @returns Array of matching nodes, or empty array on failure
93
+ */
94
+ async searchNodes(query, options) {
95
+ if (!(await this.connect()))
96
+ return [];
97
+ const groupIds = this.resolveGroupIds(options?.groupIds);
98
+ try {
99
+ const result = await this.getClient().callTool({
100
+ name: "search_nodes",
101
+ arguments: {
102
+ query,
103
+ group_ids: groupIds,
104
+ max_nodes: options?.maxResults ?? 10,
105
+ },
106
+ });
107
+ if (result.isError)
108
+ return [];
109
+ const content = result.content;
110
+ const text = content?.[0];
111
+ if (text?.text) {
112
+ const parsed = JSON.parse(text.text);
113
+ return parsed.nodes ?? parsed ?? [];
114
+ }
115
+ return [];
116
+ }
117
+ catch {
118
+ return [];
119
+ }
120
+ }
121
+ /**
122
+ * Search for facts (relationships between entities) in the knowledge graph.
123
+ *
124
+ * @param query - Natural language search query
125
+ * @param options - groupIds defaults to [projectName, "shared"] for cross-group search
126
+ * @returns Array of matching facts, or empty array on failure
127
+ */
128
+ async searchFacts(query, options) {
129
+ if (!(await this.connect()))
130
+ return [];
131
+ const groupIds = this.resolveGroupIds(options?.groupIds);
132
+ try {
133
+ const result = await this.getClient().callTool({
134
+ name: "search_memory_facts",
135
+ arguments: {
136
+ query,
137
+ group_ids: groupIds,
138
+ max_facts: options?.maxResults ?? 10,
139
+ },
140
+ });
141
+ if (result.isError)
142
+ return [];
143
+ const content = result.content;
144
+ const text = content?.[0];
145
+ if (text?.text) {
146
+ const parsed = JSON.parse(text.text);
147
+ return parsed.facts ?? parsed ?? [];
148
+ }
149
+ return [];
150
+ }
151
+ catch {
152
+ return [];
153
+ }
154
+ }
155
+ /**
156
+ * Check if the Graphiti server is reachable and healthy.
157
+ *
158
+ * @returns Status object or null if unreachable
159
+ */
160
+ async getStatus() {
161
+ if (!(await this.connect()))
162
+ return null;
163
+ try {
164
+ const result = await this.getClient().callTool({
165
+ name: "get_status",
166
+ arguments: {},
167
+ });
168
+ if (result.isError)
169
+ return null;
170
+ const content = result.content;
171
+ const text = content?.[0];
172
+ if (text?.text) {
173
+ return JSON.parse(text.text);
174
+ }
175
+ return null;
176
+ }
177
+ catch {
178
+ return null;
179
+ }
180
+ }
181
+ /**
182
+ * Resolve group IDs for search — always includes "shared" for cross-group search.
183
+ * If caller provides groupIds, uses those. Otherwise defaults to [projectName, "shared"].
184
+ * Deduplicates "shared" if already present.
185
+ */
186
+ resolveGroupIds(groupIds) {
187
+ const ids = groupIds ?? [this.projectName];
188
+ if (!ids.includes("shared")) {
189
+ ids.push("shared");
190
+ }
191
+ return ids;
192
+ }
193
+ /**
194
+ * Disconnect from the Graphiti MCP server.
195
+ */
196
+ async disconnect() {
197
+ if (this.client) {
198
+ try {
199
+ await this.client.close();
200
+ }
201
+ catch {
202
+ // ignore
203
+ }
204
+ }
205
+ this.client = null;
206
+ this.transport = null;
207
+ this.connected = false;
208
+ }
209
+ }
@@ -412,6 +412,37 @@ export function registerGraphTools(server, projectRoot) {
412
412
  });
413
413
  }
414
414
  }
415
+ // 5. Check Graphiti health
416
+ if (steps.every((s) => s.step !== "falkordb-container" || s.status !== "error")) {
417
+ try {
418
+ const health = execSync("curl -sf http://localhost:8100/health", {
419
+ encoding: "utf-8",
420
+ timeout: 5000,
421
+ stdio: ["ignore", "pipe", "pipe"],
422
+ }).trim();
423
+ if (health.includes("healthy")) {
424
+ steps.push({
425
+ step: "graphiti-health",
426
+ status: "ok",
427
+ detail: "Graphiti MCP server healthy",
428
+ });
429
+ }
430
+ else {
431
+ steps.push({
432
+ step: "graphiti-health",
433
+ status: "error",
434
+ detail: "Graphiti responded but not healthy",
435
+ });
436
+ }
437
+ }
438
+ catch {
439
+ steps.push({
440
+ step: "graphiti-health",
441
+ status: "error",
442
+ detail: "Graphiti MCP server not reachable on localhost:8100 — may still be starting (takes ~90s)",
443
+ });
444
+ }
445
+ }
415
446
  const hasErrors = steps.some((s) => s.status === "error");
416
447
  const hasFixed = steps.some((s) => s.status === "fixed");
417
448
  return {
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "graphiti",
3
+ "description": "Graphiti temporal knowledge graph — episodic memory, contradiction detection, semantic search via FalkorDB",
4
+ "provides": {
5
+ "skill": true,
6
+ "health_checks": [
7
+ {
8
+ "name": "indusk-infra-running",
9
+ "command": "docker inspect --format='{{.State.Running}}' indusk-infra 2>/dev/null | grep true"
10
+ },
11
+ {
12
+ "name": "graphiti-server-reachable",
13
+ "command": "curl -sf http://localhost:8100/health > /dev/null 2>&1"
14
+ },
15
+ {
16
+ "name": "falkordb-reachable",
17
+ "command": "redis-cli -h localhost ping 2>/dev/null | grep PONG"
18
+ }
19
+ ]
20
+ },
21
+ "detect": {
22
+ "command": "docker inspect --format='{{.State.Running}}' indusk-infra 2>/dev/null | grep true"
23
+ }
24
+ }
@@ -0,0 +1,91 @@
1
+ # Graphiti — Temporal Knowledge Graph
2
+
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
+
5
+ ## When to Use
6
+
7
+ - **Episode capture**: After a decision, retro finding, or correction — anything worth remembering across sessions
8
+ - **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
10
+
11
+ ## Core Concepts
12
+
13
+ ### 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.
15
+
16
+ ### Group IDs
17
+ Every episode belongs to a group. Groups isolate knowledge:
18
+
19
+ | Group | Purpose | Example |
20
+ |-------|---------|---------|
21
+ | `{project-name}` | Project-specific knowledge | `infinitedusky`, `numero` |
22
+ | `shared` | Cross-project conventions | Developer preferences, universal patterns |
23
+
24
+ When searching, always include both the project group and `shared` to get the full picture. The `GraphitiClient` does this automatically.
25
+
26
+ ### Entities and Facts
27
+ Graphiti extracts:
28
+ - **Entities**: Named things (tools, patterns, files, concepts)
29
+ - **Facts**: Relationships between entities with temporal validity
30
+
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.
32
+
33
+ ## Patterns
34
+
35
+ ### Capturing a Decision
36
+ After an ADR is accepted or a significant choice is made:
37
+ ```
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" })
41
+ ```
42
+
43
+ ### Capturing a Correction
44
+ When someone corrects the agent or a mistake is found:
45
+ ```
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" })
49
+ ```
50
+
51
+ ### Searching Before Acting
52
+ Before making assumptions about how something works:
53
+ ```
54
+ searchNodes("authentication middleware")
55
+ searchFacts("how does auth work in this project")
56
+ ```
57
+
58
+ ### Capturing a Retrospective Finding
59
+ After a plan retrospective surfaces a useful insight:
60
+ ```
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" })
64
+ ```
65
+
66
+ ## What NOT to Capture
67
+
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
72
+
73
+ Graphiti is for knowledge that has temporal context — decisions that might change, facts that might be contradicted, insights that accumulate over time.
74
+
75
+ ## Infrastructure
76
+
77
+ Graphiti runs inside the `indusk-infra` container alongside FalkorDB:
78
+
79
+ ```bash
80
+ indusk infra start # start the container
81
+ indusk infra status # check health
82
+ indusk infra stop # stop (preserves data)
83
+ ```
84
+
85
+ Global config (API keys, OTel): `~/.indusk/config.env`
86
+
87
+ ### Graceful Degradation
88
+ 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)
@@ -1,18 +1,6 @@
1
1
  {
2
2
  "name": "nextjs",
3
3
  "description": "Next.js 13+ patterns — App Router, server components, caching, performance",
4
- "provides": {
5
- "skill": true,
6
- "health_checks": [
7
- {
8
- "name": "no-turbopack-in-dev-scripts",
9
- "command": "! grep -rl '\\-\\-turbopack' apps/*/package.json 2>/dev/null | head -1 | grep -q ."
10
- },
11
- {
12
- "name": "webpack-aggregate-timeout-configured",
13
- "command": "for f in apps/*/next.config.*; do [ -f \"$f\" ] && grep -q 'aggregateTimeout' \"$f\" || { echo \"missing aggregateTimeout: $f\"; exit 1; }; done"
14
- }
15
- ]
16
- },
4
+ "provides": { "skill": true },
17
5
  "detect": { "dependency": "next" }
18
6
  }
@@ -24,26 +24,6 @@ You are working in a Next.js project. Follow these patterns.
24
24
  - Dynamic imports (`next/dynamic`) for heavy client components
25
25
  - Minimize `"use client"` boundaries — push them as deep in the component tree as possible
26
26
 
27
- ## Dev Server — Webpack Only
28
-
29
- - **Do not use Turbopack** — its 1ms file watcher debounce on macOS causes crashes when files are written by external tools (MCP servers, code generation, etc.)
30
- - **Next.js < 16:** plain `next dev` uses webpack by default — just don't add `--turbopack`
31
- - **Next.js 16+:** Turbopack is the default — use `next dev --webpack` to opt out
32
- - **Always include `aggregateTimeout: 600`** in webpack watchOptions in `next.config.*`
33
-
34
- ```ts
35
- // next.config.ts
36
- const nextConfig = {
37
- webpack: (config) => {
38
- config.watchOptions = {
39
- ...config.watchOptions,
40
- aggregateTimeout: 600,
41
- };
42
- return config;
43
- },
44
- };
45
- ```
46
-
47
27
  ## Common Gotchas
48
28
 
49
29
  - `"use client"` doesn't mean the component only renders on the client — it still SSRs. It means it hydrates.
@@ -189,19 +189,17 @@ Wrap this in a helper so every log call includes trace context automatically. Wi
189
189
 
190
190
  ## Error Propagation
191
191
 
192
- Errors must always include trace context. Never swallow silently.
193
-
194
- **Important: Span events (`AddEvent`, `RecordException`) are deprecated as of March 2026.** Use log-based events correlated with the active span instead. The log automatically inherits trace context when emitted inside an active span.
192
+ Errors must always include trace context. Never swallow silently:
195
193
 
196
194
  ```typescript
197
195
  try {
198
196
  await processSettlement(hand);
199
197
  } catch (err) {
200
- // 1. Set error status on span
198
+ // 1. Record on span
199
+ span.recordException(err);
201
200
  span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
202
201
 
203
- // 2. Log the error — automatically correlated with the active span
204
- // This replaces span.recordException(err) which is deprecated
202
+ // 2. Log with correlation
205
203
  logger.error({ err, ...getTraceContext() }, 'settlement failed');
206
204
 
207
205
  // 3. Re-throw — don't swallow
@@ -213,11 +211,6 @@ Every catch block should either:
213
211
  1. Re-throw with context preserved
214
212
  2. Log with trace correlation and handle completely
215
213
 
216
- **Migration from span events to logs:**
217
- - `span.addEvent('name', attrs)` → `logger.info({ ...attrs, ...getTraceContext() }, 'name')`
218
- - `span.recordException(err)` → `logger.error({ err, ...getTraceContext() }, 'error message')`
219
- - Span status (`setStatus`) is NOT deprecated — still use it to mark spans as errored
220
-
221
214
  ## Sensitive Data
222
215
 
223
216
  Never attach these to spans, logs, or metrics:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infinitedusky/indusk-mcp",
3
- "version": "1.7.0",
3
+ "version": "1.7.3",
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/handoff.md CHANGED
@@ -46,18 +46,11 @@ Create or overwrite `.claude/handoff.md` with:
46
46
 
47
47
  ## When to Write a Handoff
48
48
 
49
+ - Before ending any session where work was done
49
50
  - When the user says "let's stop here", "wrap up", "hand off"
51
+ - When you're about to run out of context
50
52
  - `/handoff` explicitly
51
53
 
52
- ## When to Suggest a Handoff
53
-
54
- Only suggest a handoff in these situations — never ask unprompted otherwise:
55
-
56
- - **You are running low on context** and can feel the conversation getting long. Say so plainly: "I'm getting close to context limits — want me to write a handoff?"
57
- - **A major milestone just completed** (e.g., a full plan phase, a feature landed, a retrospective finished) and there's no obvious next task queued up. Mention it once — if the user keeps going, don't ask again.
58
-
59
- Do **not** suggest a handoff after every task, at natural pauses, or as a closing question. The user decides when to stop.
60
-
61
54
  ## Rules
62
55
 
63
56
  - **Be specific.** "Working on Phase 3" is useless. "Phase 3, item 4: refactored check_health to use extensions. extensions_status MCP tool created. Next: refactor init to remove hardcoded FalkorDB/CGC." is useful.