@infinitedusky/indusk-mcp 1.8.0 → 1.9.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.
package/dist/bin/cli.js CHANGED
@@ -14,11 +14,13 @@ program
14
14
  .command("init")
15
15
  .description("Initialize a project with InDusk dev system")
16
16
  .option("-f, --force", "Overwrite existing files (except CLAUDE.md and planning/)")
17
+ .option("--local", "Local mode — no committed file changes")
17
18
  .option("--no-index", "Skip code graph indexing")
18
19
  .action(async (opts) => {
19
20
  const { init } = await import("./commands/init.js");
20
21
  await init(process.cwd(), {
21
22
  force: opts.force ?? false,
23
+ local: opts.local ?? false,
22
24
  noIndex: opts.index === false,
23
25
  });
24
26
  });
@@ -129,6 +131,22 @@ infra
129
131
  const { infraStatus } = await import("./commands/infra.js");
130
132
  await infraStatus();
131
133
  });
134
+ program
135
+ .command("pr-clean")
136
+ .description("Strip InDusk settings overlay before a PR")
137
+ .action(async () => {
138
+ const { stripOverlay } = await import("../lib/settings-overlay.js");
139
+ stripOverlay(process.cwd());
140
+ console.info("Stripped InDusk overlay from .claude/settings.json");
141
+ });
142
+ program
143
+ .command("pr-restore")
144
+ .description("Re-apply InDusk settings overlay after a PR")
145
+ .action(async () => {
146
+ const { applyOverlay } = await import("../lib/settings-overlay.js");
147
+ applyOverlay(process.cwd());
148
+ console.info("Re-applied InDusk overlay to .claude/settings.json");
149
+ });
132
150
  program
133
151
  .command("serve")
134
152
  .description("Start the MCP server (used by Claude Code via .mcp.json)")
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { getPlanningDir } from "../../lib/config.js";
3
4
  import { getAllPhaseCompletions, parseImpl } from "../../lib/impl-parser.js";
4
5
  export async function checkGates(projectRoot, options = {}) {
5
6
  let implPath;
@@ -7,10 +8,10 @@ export async function checkGates(projectRoot, options = {}) {
7
8
  implPath = options.file;
8
9
  }
9
10
  else {
10
- // Find active impl (in-progress status) in planning/
11
- const planningDir = join(projectRoot, "planning");
11
+ // Find active impl (in-progress status)
12
+ const planningDir = getPlanningDir(projectRoot);
12
13
  if (!existsSync(planningDir)) {
13
- console.error("No planning/ directory found");
14
+ console.error("No planning directory found");
14
15
  process.exitCode = 1;
15
16
  return;
16
17
  }
@@ -29,7 +30,7 @@ export async function checkGates(projectRoot, options = {}) {
29
30
  }
30
31
  }
31
32
  if (!found) {
32
- console.error("No in-progress impl found in planning/");
33
+ console.error("No in-progress impl found");
33
34
  process.exitCode = 1;
34
35
  return;
35
36
  }
@@ -1,5 +1,6 @@
1
1
  export interface InitOptions {
2
2
  force?: boolean;
3
+ local?: boolean;
3
4
  noIndex?: boolean;
4
5
  }
5
6
  export declare function init(projectRoot: string, options?: InitOptions): Promise<void>;
@@ -56,10 +56,82 @@ function createCgcIgnore(projectRoot) {
56
56
  ].join("\n"));
57
57
  console.info(" create: .cgcignore");
58
58
  }
59
+ function detectTooling(projectRoot) {
60
+ const detected = {};
61
+ // Detect linter
62
+ if (existsSync(join(projectRoot, "biome.json")) || existsSync(join(projectRoot, "biome.jsonc"))) {
63
+ detected.linter = "biome";
64
+ }
65
+ else if (existsSync(join(projectRoot, ".eslintrc.js")) ||
66
+ existsSync(join(projectRoot, ".eslintrc.json")) ||
67
+ existsSync(join(projectRoot, ".eslintrc.cjs")) ||
68
+ existsSync(join(projectRoot, "eslint.config.js")) ||
69
+ existsSync(join(projectRoot, "eslint.config.mjs")) ||
70
+ existsSync(join(projectRoot, "eslint.config.ts"))) {
71
+ detected.linter = "eslint";
72
+ }
73
+ // Detect test runner
74
+ if (existsSync(join(projectRoot, "vitest.config.ts")) ||
75
+ existsSync(join(projectRoot, "vitest.config.js"))) {
76
+ detected.testRunner = "vitest";
77
+ }
78
+ else if (existsSync(join(projectRoot, "jest.config.js")) ||
79
+ existsSync(join(projectRoot, "jest.config.ts"))) {
80
+ detected.testRunner = "jest";
81
+ }
82
+ // Detect OTel
83
+ if (existsSync(join(projectRoot, "instrumentation.ts")) ||
84
+ existsSync(join(projectRoot, "src/instrumentation.ts")) ||
85
+ existsSync(join(projectRoot, "instrumentation.py"))) {
86
+ detected.otel = true;
87
+ }
88
+ // Detect TypeScript
89
+ if (existsSync(join(projectRoot, "tsconfig.json"))) {
90
+ detected.typeCheck = true;
91
+ }
92
+ return detected;
93
+ }
94
+ function writeGitInfoExclude(projectRoot) {
95
+ const excludePath = join(projectRoot, ".git/info/exclude");
96
+ const marker = "# InDusk local mode";
97
+ // Ensure .git/info/ exists
98
+ mkdirSync(join(projectRoot, ".git/info"), { recursive: true });
99
+ let content = "";
100
+ if (existsSync(excludePath)) {
101
+ content = readFileSync(excludePath, "utf-8");
102
+ if (content.includes(marker))
103
+ return; // Already configured
104
+ }
105
+ const entries = [
106
+ "",
107
+ marker,
108
+ ".indusk/",
109
+ ".claude/skills/",
110
+ ".claude/hooks/",
111
+ ".claude/lessons/",
112
+ ".claude/settings.json",
113
+ ".claude/handoff.md",
114
+ ".cgcignore",
115
+ ".mcp.json",
116
+ "",
117
+ ].join("\n");
118
+ writeFileSync(excludePath, content.trimEnd() + entries);
119
+ console.info(" updated: .git/info/exclude (InDusk local mode entries)");
120
+ }
59
121
  export async function init(projectRoot, options = {}) {
60
- const { force = false, noIndex = false } = options;
122
+ const { force = false, local = false, noIndex = false } = options;
61
123
  const projectName = basename(projectRoot);
62
- console.info(`Initializing InDusk dev system...${force ? " (--force)" : ""}\n`);
124
+ const modeLabel = local ? " (--local)" : "";
125
+ console.info(`Initializing InDusk dev system...${force ? " (--force)" : ""}${modeLabel}\n`);
126
+ // Detect existing tooling
127
+ const detected = detectTooling(projectRoot);
128
+ if (local) {
129
+ console.info("[Detection]");
130
+ console.info(` linter: ${detected.linter ?? "none"}`);
131
+ console.info(` test runner: ${detected.testRunner ?? "none"}`);
132
+ console.info(` otel: ${detected.otel ? "yes" : "no"}`);
133
+ console.info(` typescript: ${detected.typeCheck ? "yes" : "no"}`);
134
+ }
63
135
  // 1. Copy skills
64
136
  console.info("[Skills]");
65
137
  const skillsSource = join(packageRoot, "skills");
@@ -95,25 +167,31 @@ export async function init(projectRoot, options = {}) {
95
167
  }
96
168
  }
97
169
  // 3. Create CLAUDE.md (never overwrite — write CLAUDE-NEW.md if exists)
98
- console.info("\n[Project files]");
99
- const claudeMdPath = join(projectRoot, "CLAUDE.md");
100
- if (existsSync(claudeMdPath)) {
101
- const newPath = join(projectRoot, "CLAUDE-NEW.md");
102
- cpSync(join(packageRoot, "templates/CLAUDE.md"), newPath);
103
- console.info(" create: CLAUDE-NEW.md (merge manually with existing CLAUDE.md)");
104
- }
105
- else {
106
- cpSync(join(packageRoot, "templates/CLAUDE.md"), claudeMdPath);
107
- console.info(" create: CLAUDE.md");
170
+ if (!local) {
171
+ console.info("\n[Project files]");
172
+ const claudeMdPath = join(projectRoot, "CLAUDE.md");
173
+ if (existsSync(claudeMdPath)) {
174
+ const newPath = join(projectRoot, "CLAUDE-NEW.md");
175
+ cpSync(join(packageRoot, "templates/CLAUDE.md"), newPath);
176
+ console.info(" create: CLAUDE-NEW.md (merge manually with existing CLAUDE.md)");
177
+ }
178
+ else {
179
+ cpSync(join(packageRoot, "templates/CLAUDE.md"), claudeMdPath);
180
+ console.info(" create: CLAUDE.md");
181
+ }
108
182
  }
109
183
  // 3. Create planning directory
110
- const planningDir = join(projectRoot, "planning");
184
+ const planningDir = join(projectRoot, ".indusk/planning");
185
+ const legacyPlanningDir = join(projectRoot, "planning");
111
186
  if (existsSync(planningDir)) {
112
- console.info(" skip: planning/ (already exists)");
187
+ console.info(" skip: .indusk/planning/ (already exists)");
188
+ }
189
+ else if (existsSync(legacyPlanningDir)) {
190
+ console.info(" migrate: planning/ → .indusk/planning/ (move manually or run: mv planning .indusk/planning)");
113
191
  }
114
192
  else {
115
193
  mkdirSync(planningDir, { recursive: true });
116
- console.info(" create: planning/");
194
+ console.info(" create: .indusk/planning/");
117
195
  }
118
196
  // 4. Set up MCP servers via claude mcp add
119
197
  console.info("\n[MCP config]");
@@ -255,29 +333,95 @@ export async function init(projectRoot, options = {}) {
255
333
  console.info(" create: .indusk/extensions/graphiti/ (manifest + skill)");
256
334
  }
257
335
  // 5. Generate .vscode/settings.json
258
- console.info("\n[Editor]");
259
- const vscodePath = join(projectRoot, ".vscode/settings.json");
260
- if (existsSync(vscodePath) && !force) {
261
- console.info(" skip: .vscode/settings.json (already exists)");
262
- }
263
- else {
264
- mkdirSync(join(projectRoot, ".vscode"), { recursive: true });
265
- cpSync(join(packageRoot, "templates/vscode-settings.json"), vscodePath);
266
- console.info(` ${existsSync(vscodePath) ? "overwrite" : "create"}: .vscode/settings.json`);
336
+ if (!local) {
337
+ console.info("\n[Editor]");
338
+ const vscodePath = join(projectRoot, ".vscode/settings.json");
339
+ if (existsSync(vscodePath) && !force) {
340
+ console.info(" skip: .vscode/settings.json (already exists)");
341
+ }
342
+ else {
343
+ mkdirSync(join(projectRoot, ".vscode"), { recursive: true });
344
+ cpSync(join(packageRoot, "templates/vscode-settings.json"), vscodePath);
345
+ console.info(` ${existsSync(vscodePath) ? "overwrite" : "create"}: .vscode/settings.json`);
346
+ }
267
347
  }
268
- // 6. Create base biome.json
269
- const biomePath = join(projectRoot, "biome.json");
270
- if (existsSync(biomePath) && !force) {
271
- console.info(" skip: biome.json (already exists)");
348
+ // 6. Create biome.json (root in full mode, .indusk/ in local mode)
349
+ if (!local) {
350
+ const biomePath = join(projectRoot, "biome.json");
351
+ if (existsSync(biomePath) && !force) {
352
+ console.info(" skip: biome.json (already exists)");
353
+ }
354
+ else {
355
+ cpSync(join(packageRoot, "templates/biome.template.json"), biomePath);
356
+ console.info(` ${existsSync(biomePath) ? "overwrite" : "create"}: biome.json`);
357
+ }
272
358
  }
273
359
  else {
274
- cpSync(join(packageRoot, "templates/biome.template.json"), biomePath);
275
- console.info(` ${existsSync(biomePath) ? "overwrite" : "create"}: biome.json`);
360
+ console.info("\n[Local Quality Tools]");
361
+ // Biome in .indusk/
362
+ const localBiomePath = join(projectRoot, ".indusk/biome.json");
363
+ if (existsSync(localBiomePath) && !force) {
364
+ console.info(" skip: .indusk/biome.json (already exists)");
365
+ }
366
+ else {
367
+ cpSync(join(packageRoot, "templates/biome.template.json"), localBiomePath);
368
+ console.info(" create: .indusk/biome.json");
369
+ }
370
+ // Test runner config in .indusk/
371
+ if (detected.testRunner === "jest") {
372
+ const jestConfig = join(projectRoot, ".indusk/jest.config.js");
373
+ if (!existsSync(jestConfig) || force) {
374
+ writeFileSync(jestConfig, [
375
+ "/** @type {import('jest').Config} */",
376
+ "module.exports = {",
377
+ " roots: ['<rootDir>/../', '<rootDir>/tests/'],",
378
+ " testMatch: ['<rootDir>/tests/**/*.test.{js,ts}'],",
379
+ "};",
380
+ "",
381
+ ].join("\n"));
382
+ console.info(" create: .indusk/jest.config.js (extends team roots)");
383
+ }
384
+ }
385
+ else {
386
+ // Default to vitest
387
+ const vitestConfig = join(projectRoot, ".indusk/vitest.config.ts");
388
+ if (!existsSync(vitestConfig) || force) {
389
+ writeFileSync(vitestConfig, [
390
+ 'import { defineConfig } from "vitest/config";',
391
+ "",
392
+ "export default defineConfig({",
393
+ " test: {",
394
+ ' include: [".indusk/tests/**/*.test.{ts,js}"],',
395
+ " passWithNoTests: true,",
396
+ " },",
397
+ "});",
398
+ "",
399
+ ].join("\n"));
400
+ console.info(" create: .indusk/vitest.config.ts");
401
+ }
402
+ }
403
+ // Tests directory
404
+ const testsDir = join(projectRoot, ".indusk/tests");
405
+ if (!existsSync(testsDir)) {
406
+ mkdirSync(testsDir, { recursive: true });
407
+ writeFileSync(join(testsDir, ".gitkeep"), "");
408
+ console.info(" create: .indusk/tests/");
409
+ }
410
+ // Docs directory
411
+ const docsDir = join(projectRoot, ".indusk/docs");
412
+ if (!existsSync(docsDir)) {
413
+ mkdirSync(docsDir, { recursive: true });
414
+ writeFileSync(join(docsDir, "index.md"), `# ${projectName}\n\nLocal documentation. Portable to VitePress later.\n`);
415
+ console.info(" create: .indusk/docs/ (with index.md)");
416
+ }
276
417
  }
277
- // 7. Scaffold OpenTelemetry instrumentation
278
- // Skip if this is the indusk-mcp package itself (has templates/ directory with instrumentation.ts)
418
+ // 7. Scaffold OpenTelemetry instrumentation (skip in local mode)
279
419
  const isInduskMcp = existsSync(join(projectRoot, "templates/instrumentation.ts"));
280
- if (isInduskMcp) {
420
+ if (local) {
421
+ console.info("\n[OpenTelemetry]");
422
+ console.info(" skip: local mode (team owns OTel setup)");
423
+ }
424
+ else if (isInduskMcp) {
281
425
  console.info("\n[OpenTelemetry]");
282
426
  console.info(" skip: this is the indusk-mcp package (templates are source, not scaffolded)");
283
427
  }
@@ -421,6 +565,7 @@ export async function init(projectRoot, options = {}) {
421
565
  }
422
566
  }
423
567
  // Merge hook config + permissions into .claude/settings.json
568
+ const { writeOverlay, applyOverlay } = await import("../../lib/settings-overlay.js");
424
569
  const claudeSettingsPath = join(projectRoot, ".claude/settings.json");
425
570
  const catchupPermissions = [
426
571
  "mcp__indusk__list_lessons",
@@ -507,9 +652,25 @@ export async function init(projectRoot, options = {}) {
507
652
  writeFileSync(claudeSettingsPath, `${JSON.stringify(settings, null, "\t")}\n`);
508
653
  console.info(" create: .claude/settings.json (with hook config + catchup permissions)");
509
654
  }
510
- // 8. Create .cgcignore (always overwrite package-owned)
655
+ // In local mode, save overlay so we can strip our additions before PRs
656
+ if (local) {
657
+ const overlayData = {
658
+ permissions: { allow: catchupPermissions },
659
+ hooks: hookConfig,
660
+ };
661
+ writeOverlay(projectRoot, overlayData);
662
+ applyOverlay(projectRoot);
663
+ console.info(" saved: .indusk/settings-overlay.json");
664
+ }
665
+ // 8. Create .cgcignore and manage git excludes
511
666
  createCgcIgnore(projectRoot);
512
- ensureGitignoreMcpJson(projectRoot);
667
+ if (local) {
668
+ console.info("\n[Git Excludes]");
669
+ writeGitInfoExclude(projectRoot);
670
+ }
671
+ else {
672
+ ensureGitignoreMcpJson(projectRoot);
673
+ }
513
674
  // 9. Run on_init hooks from enabled extensions
514
675
  console.info("\n[Extension Hooks]");
515
676
  const { getEnabledExtensions } = await import("../../lib/extension-loader.js");
@@ -568,6 +729,30 @@ export async function init(projectRoot, options = {}) {
568
729
  console.info("\n[Extensions]");
569
730
  const { autoEnableExtensions } = await import("./extensions.js");
570
731
  await autoEnableExtensions(projectRoot);
732
+ // 12. Write .indusk/config.json
733
+ const { writeConfig } = await import("../../lib/config.js");
734
+ const linterTool = local ? "biome" : (detected.linter ?? "biome");
735
+ const linterConfig = local ? ".indusk/biome.json" : "biome.json";
736
+ const testTool = detected.testRunner ?? "vitest";
737
+ const testConfig = local
738
+ ? `.indusk/${testTool === "jest" ? "jest.config.js" : "vitest.config.ts"}`
739
+ : `${testTool}.config.${testTool === "jest" ? "js" : "ts"}`;
740
+ const config = {
741
+ mode: local ? "local" : "full",
742
+ verify: {
743
+ linter: { tool: linterTool, config: linterConfig },
744
+ testRunner: { tool: testTool, config: testConfig },
745
+ ...(detected.typeCheck ? { typeCheck: "tsc" } : {}),
746
+ },
747
+ detected: {
748
+ ...(detected.linter ? { linter: detected.linter } : {}),
749
+ ...(detected.testRunner ? { testRunner: detected.testRunner } : {}),
750
+ ...(detected.otel ? { otel: true } : {}),
751
+ },
752
+ };
753
+ writeConfig(projectRoot, config);
754
+ console.info(`\n[Config]`);
755
+ console.info(` create: .indusk/config.json (mode: ${config.mode})`);
571
756
  // Summary
572
757
  console.info("\nDone!");
573
758
  console.info("\n⚠ Restart Claude Code to load the updated MCP server and skills.");
@@ -1,6 +1,6 @@
1
- import { createHash } from "node:crypto";
2
1
  import { execSync } from "node:child_process";
3
- import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
4
4
  import { dirname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { globSync } from "glob";
@@ -186,7 +186,12 @@ export async function update(projectRoot) {
186
186
  let hooksUpdated = 0;
187
187
  let hooksCurrent = 0;
188
188
  if (existsSync(hooksSource) && existsSync(hooksTarget)) {
189
- const hookFiles = ["check-gates.js", "gate-reminder.js", "validate-impl-structure.js", "check-catchup.js"];
189
+ const hookFiles = [
190
+ "check-gates.js",
191
+ "gate-reminder.js",
192
+ "validate-impl-structure.js",
193
+ "check-catchup.js",
194
+ ];
190
195
  for (const file of hookFiles) {
191
196
  const sourceFile = join(hooksSource, file);
192
197
  const targetFile = join(hooksTarget, file);
@@ -252,20 +257,21 @@ export async function update(projectRoot) {
252
257
  cpSync(builtinSkill, targetSkill);
253
258
  console.info(` added: ${name} skill`);
254
259
  }
255
- // Run on_update hook if present
260
+ // Run update hooks if present
256
261
  const manifest = loadExtension(enabledManifest);
257
- if (manifest?.hooks?.on_update) {
258
- console.info(` ${name}: running on_update hook...`);
262
+ const updateHook = manifest?.hooks?.on_update ?? manifest?.hooks?.on_post_update;
263
+ if (updateHook) {
264
+ console.info(` ${name}: running update hook...`);
259
265
  try {
260
- execSync(manifest.hooks.on_update, {
266
+ execSync(updateHook, {
261
267
  cwd: projectRoot,
262
268
  timeout: 30000,
263
269
  stdio: ["ignore", "pipe", "pipe"],
264
270
  });
265
- console.info(` ${name}: on_update hook completed`);
271
+ console.info(` ${name}: update hook completed`);
266
272
  }
267
273
  catch {
268
- console.info(` ${name}: on_update hook failed`);
274
+ console.info(` ${name}: update hook failed`);
269
275
  }
270
276
  }
271
277
  if (manifest?.mcp_server?.setup_instructions) {
@@ -286,6 +292,15 @@ export async function update(projectRoot) {
286
292
  catch {
287
293
  console.info(" could not check third-party extensions");
288
294
  }
295
+ // 8. Respect local mode: re-apply overlay, refresh excludes
296
+ const { readConfig } = await import("../../lib/config.js");
297
+ const config = readConfig(projectRoot);
298
+ if (config?.mode === "local") {
299
+ console.info("\n[Local Mode]\n");
300
+ const { applyOverlay } = await import("../../lib/settings-overlay.js");
301
+ applyOverlay(projectRoot);
302
+ console.info(" re-applied settings overlay");
303
+ }
289
304
  console.info("\nDone.");
290
305
  if (didUpgrade) {
291
306
  console.info("Restart Claude Code to pick up the new MCP server.");
@@ -0,0 +1,21 @@
1
+ export interface VerifyToolConfig {
2
+ tool: string;
3
+ config: string;
4
+ }
5
+ export interface InduskConfig {
6
+ mode: "full" | "local";
7
+ verify: {
8
+ linter?: VerifyToolConfig;
9
+ testRunner?: VerifyToolConfig;
10
+ typeCheck?: string;
11
+ };
12
+ detected: {
13
+ otel?: boolean;
14
+ testRunner?: string;
15
+ linter?: string;
16
+ };
17
+ }
18
+ export declare function getConfigPath(projectRoot: string): string;
19
+ export declare function readConfig(projectRoot: string): InduskConfig | null;
20
+ export declare function writeConfig(projectRoot: string, config: InduskConfig): void;
21
+ export declare function getPlanningDir(projectRoot: string): string;
@@ -0,0 +1,28 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ const CONFIG_PATH = ".indusk/config.json";
4
+ export function getConfigPath(projectRoot) {
5
+ return join(projectRoot, CONFIG_PATH);
6
+ }
7
+ export function readConfig(projectRoot) {
8
+ const configPath = getConfigPath(projectRoot);
9
+ if (!existsSync(configPath))
10
+ return null;
11
+ return JSON.parse(readFileSync(configPath, "utf-8"));
12
+ }
13
+ export function writeConfig(projectRoot, config) {
14
+ const configPath = getConfigPath(projectRoot);
15
+ mkdirSync(dirname(configPath), { recursive: true });
16
+ writeFileSync(configPath, `${JSON.stringify(config, null, "\t")}\n`);
17
+ }
18
+ export function getPlanningDir(projectRoot) {
19
+ const newPath = join(projectRoot, ".indusk/planning");
20
+ const legacyPath = join(projectRoot, "planning");
21
+ // Prefer .indusk/planning, fall back to legacy planning/ for migration
22
+ if (existsSync(newPath))
23
+ return newPath;
24
+ if (existsSync(legacyPath))
25
+ return legacyPath;
26
+ // Default to new path (will be created by init)
27
+ return newPath;
28
+ }
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import matter from "gray-matter";
4
+ import { getPlanningDir } from "./config.js";
4
5
  const STAGE_ORDER = ["research", "brief", "adr", "impl", "retrospective"];
5
6
  function parseFrontmatter(filePath) {
6
7
  if (!existsSync(filePath))
@@ -22,7 +23,7 @@ function parseDependsOn(filePath) {
22
23
  return [];
23
24
  const deps = [];
24
25
  for (const line of depsMatch[1].split("\n")) {
25
- const match = line.match(/^-\s+`?planning\/([^/`]+)\/?`?/);
26
+ const match = line.match(/^-\s+`?(?:\.indusk\/)?planning\/([^/`]+)\/?`?/);
26
27
  if (match) {
27
28
  deps.push(match[1]);
28
29
  }
@@ -72,7 +73,7 @@ export function parsePlan(planDir) {
72
73
  };
73
74
  }
74
75
  export function parseAllPlans(projectRoot) {
75
- const planningDir = join(projectRoot, "planning");
76
+ const planningDir = getPlanningDir(projectRoot);
76
77
  if (!existsSync(planningDir))
77
78
  return [];
78
79
  return readdirSync(planningDir, { withFileTypes: true })
@@ -0,0 +1,13 @@
1
+ export declare function getOverlayPath(projectRoot: string): string;
2
+ export declare function writeOverlay(projectRoot: string, additions: Record<string, unknown>): void;
3
+ export declare function readOverlay(projectRoot: string): Record<string, unknown> | null;
4
+ /**
5
+ * Merge overlay additions into .claude/settings.json.
6
+ * Deep-merges objects, concatenates arrays (deduplicating strings).
7
+ */
8
+ export declare function applyOverlay(projectRoot: string): void;
9
+ /**
10
+ * Remove overlay additions from .claude/settings.json.
11
+ * Strips keys/values that came from the overlay.
12
+ */
13
+ export declare function stripOverlay(projectRoot: string): void;
@@ -0,0 +1,101 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ const OVERLAY_PATH = ".indusk/settings-overlay.json";
4
+ const SETTINGS_PATH = ".claude/settings.json";
5
+ export function getOverlayPath(projectRoot) {
6
+ return join(projectRoot, OVERLAY_PATH);
7
+ }
8
+ export function writeOverlay(projectRoot, additions) {
9
+ const overlayPath = getOverlayPath(projectRoot);
10
+ mkdirSync(dirname(overlayPath), { recursive: true });
11
+ writeFileSync(overlayPath, `${JSON.stringify(additions, null, "\t")}\n`);
12
+ }
13
+ export function readOverlay(projectRoot) {
14
+ const overlayPath = getOverlayPath(projectRoot);
15
+ if (!existsSync(overlayPath))
16
+ return null;
17
+ return JSON.parse(readFileSync(overlayPath, "utf-8"));
18
+ }
19
+ /**
20
+ * Merge overlay additions into .claude/settings.json.
21
+ * Deep-merges objects, concatenates arrays (deduplicating strings).
22
+ */
23
+ export function applyOverlay(projectRoot) {
24
+ const overlay = readOverlay(projectRoot);
25
+ if (!overlay)
26
+ return;
27
+ const settingsPath = join(projectRoot, SETTINGS_PATH);
28
+ const existing = existsSync(settingsPath) ? JSON.parse(readFileSync(settingsPath, "utf-8")) : {};
29
+ const merged = deepMerge(existing, overlay);
30
+ mkdirSync(dirname(settingsPath), { recursive: true });
31
+ writeFileSync(settingsPath, `${JSON.stringify(merged, null, "\t")}\n`);
32
+ }
33
+ /**
34
+ * Remove overlay additions from .claude/settings.json.
35
+ * Strips keys/values that came from the overlay.
36
+ */
37
+ export function stripOverlay(projectRoot) {
38
+ const overlay = readOverlay(projectRoot);
39
+ if (!overlay)
40
+ return;
41
+ const settingsPath = join(projectRoot, SETTINGS_PATH);
42
+ if (!existsSync(settingsPath))
43
+ return;
44
+ const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
45
+ const cleaned = deepStrip(settings, overlay);
46
+ writeFileSync(settingsPath, `${JSON.stringify(cleaned, null, "\t")}\n`);
47
+ }
48
+ function deepMerge(target, source) {
49
+ const result = { ...target };
50
+ for (const [key, sourceVal] of Object.entries(source)) {
51
+ const targetVal = result[key];
52
+ if (Array.isArray(sourceVal) && Array.isArray(targetVal)) {
53
+ // Concatenate arrays, dedup strings
54
+ const combined = [...targetVal];
55
+ for (const item of sourceVal) {
56
+ if (typeof item === "string") {
57
+ if (!combined.includes(item))
58
+ combined.push(item);
59
+ }
60
+ else {
61
+ combined.push(item);
62
+ }
63
+ }
64
+ result[key] = combined;
65
+ }
66
+ else if (isObject(sourceVal) && isObject(targetVal)) {
67
+ result[key] = deepMerge(targetVal, sourceVal);
68
+ }
69
+ else {
70
+ result[key] = sourceVal;
71
+ }
72
+ }
73
+ return result;
74
+ }
75
+ function deepStrip(target, overlay) {
76
+ const result = { ...target };
77
+ for (const [key, overlayVal] of Object.entries(overlay)) {
78
+ const targetVal = result[key];
79
+ if (Array.isArray(overlayVal) && Array.isArray(targetVal)) {
80
+ // Remove overlay items from array
81
+ result[key] = targetVal.filter((item) => {
82
+ if (typeof item === "string") {
83
+ return !overlayVal.includes(item);
84
+ }
85
+ // For objects (hook entries), compare by JSON serialization
86
+ const itemStr = JSON.stringify(item);
87
+ return !overlayVal.some((ov) => JSON.stringify(ov) === itemStr);
88
+ });
89
+ }
90
+ else if (isObject(overlayVal) && isObject(targetVal)) {
91
+ result[key] = deepStrip(targetVal, overlayVal);
92
+ }
93
+ else {
94
+ delete result[key];
95
+ }
96
+ }
97
+ return result;
98
+ }
99
+ function isObject(val) {
100
+ return typeof val === "object" && val !== null && !Array.isArray(val);
101
+ }
@@ -1,5 +1,6 @@
1
1
  import { join } from "node:path";
2
2
  import { z } from "zod";
3
+ import { getPlanningDir } from "../lib/config.js";
3
4
  import { getAllPhaseCompletions, parseImpl } from "../lib/impl-parser.js";
4
5
  import { parseAllPlans, parsePlan } from "../lib/plan-parser.js";
5
6
  export function registerPlanTools(server, projectRoot) {
@@ -15,7 +16,7 @@ export function registerPlanTools(server, projectRoot) {
15
16
  description: "Get detailed status of a specific plan including phase progress and blocked items",
16
17
  inputSchema: { name: z.string().describe("Plan directory name (e.g. 'mcp-dev-system')") },
17
18
  }, async ({ name }) => {
18
- const planDir = join(projectRoot, "planning", name);
19
+ const planDir = join(getPlanningDir(projectRoot), name);
19
20
  const plan = parsePlan(planDir);
20
21
  const implPath = join(planDir, "impl.md");
21
22
  const impl = parseImpl(implPath);
@@ -33,7 +34,7 @@ export function registerPlanTools(server, projectRoot) {
33
34
  description: "Validate whether a plan can advance to the next stage. Returns what is missing if blocked.",
34
35
  inputSchema: { name: z.string().describe("Plan directory name") },
35
36
  }, async ({ name }) => {
36
- const planDir = join(projectRoot, "planning", name);
37
+ const planDir = join(getPlanningDir(projectRoot), name);
37
38
  const plan = parsePlan(planDir);
38
39
  const implPath = join(planDir, "impl.md");
39
40
  const impl = parseImpl(implPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infinitedusky/indusk-mcp",
3
- "version": "1.8.0",
3
+ "version": "1.9.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/context.md CHANGED
@@ -30,7 +30,7 @@ CLAUDE.md has exactly six sections. This structure is fixed — never add, remov
30
30
  {Patterns to follow, anti-patterns to avoid. Accumulated from corrections, retrospectives, and explicit decisions. Each entry is a concise one-liner.}
31
31
 
32
32
  ## Key Decisions
33
- {One-liner per decision with a link to the source document. Format: "- {decision summary} — see planning/{plan}/adr.md"}
33
+ {One-liner per decision with a link to the source document. Format: "- {decision summary} — see .indusk/planning/{plan}/adr.md"}
34
34
 
35
35
  ## Known Gotchas
36
36
  {Things that went wrong before. Mistakes the agent made and was corrected on. Each entry is a concise one-liner explaining what NOT to do and why.}
@@ -87,7 +87,7 @@ Do this immediately after writing the retrospective, before moving on.
87
87
  When an ADR's status changes to `accepted`, add a one-liner to **Key Decisions**:
88
88
 
89
89
  ```markdown
90
- - {Concise decision summary} — see `planning/{plan-name}/adr.md`
90
+ - {Concise decision summary} — see `.indusk/planning/{plan-name}/adr.md`
91
91
  ```
92
92
 
93
93
  Do not duplicate the ADR's rationale. The link is the documentation.
package/skills/plan.md CHANGED
@@ -8,7 +8,7 @@ You know how to plan work in this project.
8
8
 
9
9
  ## How Plans Work Here
10
10
 
11
- Every plan lives in `planning/{kebab-case-name}/` and follows the same document lifecycle:
11
+ Every plan lives in `.indusk/planning/{kebab-case-name}/` and follows the same document lifecycle:
12
12
 
13
13
  ```
14
14
  research.md → brief.md → adr.md → impl.md → retrospective.md
@@ -63,7 +63,7 @@ Workflow templates are in `templates/workflows/` in the package. They describe w
63
63
  - **spike**: start with research (and stop there)
64
64
 
65
65
  **Check for existing research first.** Before writing new research, scan `research/` at the repo root for relevant standalone research docs. If one exists (e.g., `research/auth-options.md`), ask the user: "I found existing research at `research/auth-options.md`. Want to use this as the starting point?" If yes:
66
- - Copy it to `planning/{plan-name}/research.md`
66
+ - Copy it to `.indusk/planning/{plan-name}/research.md`
67
67
  - Set the frontmatter status to `complete`
68
68
  - Move straight to the brief
69
69
 
@@ -74,7 +74,7 @@ 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
- 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 planning/{plan}/adr.md`
77
+ 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
78
 
79
79
  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
80
 
@@ -91,7 +91,7 @@ Workflow templates are in `templates/workflows/` in the package. They describe w
91
91
  ## Cross-Referencing Between Plans
92
92
 
93
93
  Plans frequently depend on or relate to each other. When work overlaps:
94
- - Reference related plans by path: "See `planning/security-hardening/` Phase 8"
94
+ - Reference related plans by path: "See `.indusk/planning/security-hardening/` Phase 8"
95
95
  - Use the `## Depends On` / `## Blocks` sections in the brief to make ordering explicit
96
96
  - If a change in one plan affects another, update both — don't let them drift
97
97
 
@@ -154,10 +154,10 @@ status: draft | accepted
154
154
  - {How we know this worked}
155
155
 
156
156
  ## Depends On
157
- - {Plans that must be completed before this one — e.g., `planning/per-game-escrow/`}
157
+ - {Plans that must be completed before this one — e.g., `.indusk/planning/per-game-escrow/`}
158
158
 
159
159
  ## Blocks
160
- - {Plans that are waiting on this one — e.g., `planning/electric-ledger-sync/`}
160
+ - {Plans that are waiting on this one — e.g., `.indusk/planning/electric-ledger-sync/`}
161
161
  ```
162
162
 
163
163
  ### adr.md
@@ -323,7 +323,7 @@ date: {YYYY-MM-DD}
323
323
  ## Folder Conventions
324
324
 
325
325
  ```
326
- planning/
326
+ .indusk/planning/
327
327
  ├── {plan-name}/
328
328
  │ ├── research.md
329
329
  │ ├── brief.md
@@ -337,8 +337,8 @@ research/ # Standalone insights useful across plans
337
337
  ```
338
338
 
339
339
  - Kebab-case folder names
340
- - Archive completed/abandoned plans to `planning/archive/`
341
- - When revising, archive the old version first (`planning/archive/{name}_v1/`)
340
+ - Archive completed/abandoned plans to `.indusk/planning/archive/`
341
+ - When revising, archive the old version first (`.indusk/planning/archive/{name}_v1/`)
342
342
 
343
343
  ## Important
344
344
 
@@ -30,7 +30,7 @@ Work through these steps in order. Each step is blocking — do not skip ahead.
30
30
 
31
31
  ### Step 1: Write the Retrospective Document
32
32
 
33
- Create `planning/{plan-name}/retrospective.md` using the template from the plan skill. This is the reflective writing — what we set out to do, what actually happened, what we learned.
33
+ Create `.indusk/planning/{plan-name}/retrospective.md` using the template from the plan skill. This is the reflective writing — what we set out to do, what actually happened, what we learned.
34
34
 
35
35
  Key sections to fill in honestly:
36
36
  - **What We Set Out to Do** — recap from the brief
@@ -110,7 +110,7 @@ Distill planning artifacts into the docs site so the knowledge survives archival
110
110
  **ADR → Decisions page:**
111
111
  Create `apps/indusk-docs/src/decisions/{plan-name}.md` with:
112
112
  - A concise summary of what was decided and why
113
- - Link to the full ADR in the archive: `planning/archive/{plan-name}/adr.md`
113
+ - Link to the full ADR in the archive: `.indusk/planning/archive/{plan-name}/adr.md`
114
114
  - Key tradeoffs accepted
115
115
 
116
116
  **Retrospective insights → Lessons page:**
@@ -127,8 +127,8 @@ Not every plan produces a lessons page — only create one if the insights are g
127
127
  Move the planning artifacts to the archive:
128
128
 
129
129
  ```bash
130
- mkdir -p planning/archive
131
- mv planning/{plan-name} planning/archive/{plan-name}
130
+ mkdir -p .indusk/planning/archive
131
+ mv .indusk/planning/{plan-name} .indusk/planning/archive/{plan-name}
132
132
  ```
133
133
 
134
134
  The docs site now holds the published knowledge. The archive holds the process history. Both are preserved, but the docs are the primary reference going forward.
package/skills/work.md CHANGED
@@ -8,11 +8,11 @@ You know how to execute plans in this project.
8
8
 
9
9
  ## How Work Works Here
10
10
 
11
- Implementation plans live in `planning/{plan-name}/impl.md` as checklists. Your job is to work through them methodically — one item at a time, in order, checking each off immediately after completing it.
11
+ Implementation plans live in `.indusk/planning/{plan-name}/impl.md` as checklists. Your job is to work through them methodically — one item at a time, in order, checking each off immediately after completing it.
12
12
 
13
13
  ## What to Do When Asked to Work
14
14
 
15
- 1. **Find the right plan.** Look in `planning/` for the plan matching what the user asked for. If they didn't specify, list all plans that have an impl with status `approved` or `in-progress` and ask which one.
15
+ 1. **Find the right plan.** Look in `.indusk/planning/` for the plan matching what the user asked for. If they didn't specify, list all plans that have an impl with status `approved` or `in-progress` and ask which one.
16
16
 
17
17
  2. **Check prerequisites.** Before starting work:
18
18
  - If the plan has an ADR, verify its status is `accepted`. If it's still `proposed`, warn the user: "The ADR hasn't been accepted yet — want to review it first, or proceed anyway?"