@infinitedusky/indusk-mcp 1.7.9 → 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 +18 -0
- package/dist/bin/commands/check-gates.js +5 -4
- package/dist/bin/commands/init.d.ts +1 -0
- package/dist/bin/commands/init.js +220 -35
- package/dist/bin/commands/update.js +33 -3
- package/dist/lib/config.d.ts +21 -0
- package/dist/lib/config.js +28 -0
- package/dist/lib/plan-parser.js +3 -2
- package/dist/lib/settings-overlay.d.ts +13 -0
- package/dist/lib/settings-overlay.js +101 -0
- package/dist/tools/plan-tools.js +3 -2
- package/package.json +1 -1
- package/skills/context.md +2 -2
- package/skills/plan.md +9 -9
- package/skills/retrospective.md +4 -4
- package/skills/work.md +2 -2
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)
|
|
11
|
-
const planningDir =
|
|
11
|
+
// Find active impl (in-progress status)
|
|
12
|
+
const planningDir = getPlanningDir(projectRoot);
|
|
12
13
|
if (!existsSync(planningDir)) {
|
|
13
|
-
console.error("No planning
|
|
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
|
|
33
|
+
console.error("No in-progress impl found");
|
|
33
34
|
process.exitCode = 1;
|
|
34
35
|
return;
|
|
35
36
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
275
|
-
|
|
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 (
|
|
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
|
-
//
|
|
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
|
-
|
|
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 {
|
|
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 = [
|
|
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,7 +257,23 @@ export async function update(projectRoot) {
|
|
|
252
257
|
cpSync(builtinSkill, targetSkill);
|
|
253
258
|
console.info(` added: ${name} skill`);
|
|
254
259
|
}
|
|
260
|
+
// Run update hooks if present
|
|
255
261
|
const manifest = loadExtension(enabledManifest);
|
|
262
|
+
const updateHook = manifest?.hooks?.on_update ?? manifest?.hooks?.on_post_update;
|
|
263
|
+
if (updateHook) {
|
|
264
|
+
console.info(` ${name}: running update hook...`);
|
|
265
|
+
try {
|
|
266
|
+
execSync(updateHook, {
|
|
267
|
+
cwd: projectRoot,
|
|
268
|
+
timeout: 30000,
|
|
269
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
270
|
+
});
|
|
271
|
+
console.info(` ${name}: update hook completed`);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
console.info(` ${name}: update hook failed`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
256
277
|
if (manifest?.mcp_server?.setup_instructions) {
|
|
257
278
|
console.info(` ${name}: MCP server setup — see .claude/skills/${name}/SKILL.md`);
|
|
258
279
|
}
|
|
@@ -271,6 +292,15 @@ export async function update(projectRoot) {
|
|
|
271
292
|
catch {
|
|
272
293
|
console.info(" could not check third-party extensions");
|
|
273
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
|
+
}
|
|
274
304
|
console.info("\nDone.");
|
|
275
305
|
if (didUpgrade) {
|
|
276
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
|
+
}
|
package/dist/lib/plan-parser.js
CHANGED
|
@@ -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 =
|
|
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
|
+
}
|
package/dist/tools/plan-tools.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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
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
|
|
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
|
|
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
|
|
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
|
|
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.,
|
|
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.,
|
|
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
|
|
341
|
-
- When revising, archive the old version first (
|
|
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
|
|
package/skills/retrospective.md
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
|
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
|
|
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?"
|