@infinitedusky/indusk-mcp 1.10.0 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,6 +22,23 @@ export interface InduskConfig {
22
22
  */
23
23
  groupId?: string;
24
24
  };
25
+ otel?: {
26
+ /**
27
+ * Project's relationship to OpenTelemetry. Controls whether the OTel gate
28
+ * fires when the planner writes impl phases and when the validate-impl-structure
29
+ * / check-gates hooks evaluate them.
30
+ *
31
+ * - `service`: produces telemetry I want to collect (default behavior; gate fires)
32
+ * - `library`: ships to other people, never produces telemetry (gate silent)
33
+ * - `tool`: short-lived script, telemetry overhead exceeds value (gate silent)
34
+ * - `none`: explicit opt-out for legacy/prototype/internal experiments (gate silent)
35
+ *
36
+ * **If unset, behaves as `service`** (gate fires). This preserves backwards
37
+ * compatibility — existing projects without the field continue to get the OTel
38
+ * gate enforced. Opt-out is explicit; opt-in is implicit.
39
+ */
40
+ role?: "service" | "library" | "tool" | "none";
41
+ };
25
42
  }
26
43
  export declare function getConfigPath(projectRoot: string): string;
27
44
  export declare function readConfig(projectRoot: string): InduskConfig | null;
@@ -38,3 +55,18 @@ export declare function getPlanningDir(projectRoot: string): string;
38
55
  * searching Graphiti — this gives both project-scoped and cross-project knowledge.
39
56
  */
40
57
  export declare function getProjectGroupId(projectRoot: string): string;
58
+ /**
59
+ * Whether the OTel gate should fire for this project.
60
+ *
61
+ * Returns `true` if `.indusk/config.json` is missing, missing `otel.role`, or
62
+ * has `otel.role: "service"`. Returns `false` only when the project explicitly
63
+ * opts out via `otel.role: "library" | "tool" | "none"`.
64
+ *
65
+ * Used by:
66
+ * - planner skill (whether to write `#### Phase N OTel` sections into impl.md)
67
+ * - validate-impl-structure hook (whether to require an OTel section at write time)
68
+ * - check-gates hook (whether to block phase advancement on missing OTel)
69
+ *
70
+ * Backwards compatible: projects without the new field behave exactly as before.
71
+ */
72
+ export declare function shouldEmitOtelGate(projectRoot: string): boolean;
@@ -42,3 +42,22 @@ export function getProjectGroupId(projectRoot) {
42
42
  return config.graphiti.groupId;
43
43
  return basename(projectRoot);
44
44
  }
45
+ /**
46
+ * Whether the OTel gate should fire for this project.
47
+ *
48
+ * Returns `true` if `.indusk/config.json` is missing, missing `otel.role`, or
49
+ * has `otel.role: "service"`. Returns `false` only when the project explicitly
50
+ * opts out via `otel.role: "library" | "tool" | "none"`.
51
+ *
52
+ * Used by:
53
+ * - planner skill (whether to write `#### Phase N OTel` sections into impl.md)
54
+ * - validate-impl-structure hook (whether to require an OTel section at write time)
55
+ * - check-gates hook (whether to block phase advancement on missing OTel)
56
+ *
57
+ * Backwards compatible: projects without the new field behave exactly as before.
58
+ */
59
+ export function shouldEmitOtelGate(projectRoot) {
60
+ const config = readConfig(projectRoot);
61
+ const role = config?.otel?.role;
62
+ return role === undefined || role === "service";
63
+ }
@@ -2,11 +2,16 @@
2
2
  /**
3
3
  * PreToolUse hook: blocks phase transitions in impl.md when gates are incomplete.
4
4
  *
5
+ * The OTel gate is conditional on the project's `otel.role` in .indusk/config.json:
6
+ * - unset or "service": OTel gate is enforced (default)
7
+ * - "library" / "tool" / "none": OTel gate is silenced (mirrors validate-impl-structure)
8
+ *
5
9
  * Exit 0 = allow the edit
6
10
  * Exit 2 = block the edit (stderr sent to agent as feedback)
7
11
  */
8
12
 
9
- import { readFileSync } from "node:fs";
13
+ import { existsSync, readFileSync } from "node:fs";
14
+ import { resolve } from "node:path";
10
15
 
11
16
  // Read hook input from stdin
12
17
  let input = "";
@@ -115,14 +120,57 @@ function detectWorkflow(content) {
115
120
  return m ? m[1] : "feature";
116
121
  }
117
122
 
118
- // Which gate types are required per workflow
119
- const WORKFLOW_GATES = {
123
+ /**
124
+ * Find the project root by walking up from a starting directory looking for
125
+ * a .indusk/ or .claude/ directory. Falls back to startDir if none found.
126
+ */
127
+ function findProjectRoot(startDir) {
128
+ let dir = startDir;
129
+ for (let i = 0; i < 10; i++) {
130
+ if (existsSync(`${dir}/.indusk`) || existsSync(`${dir}/.claude`)) return dir;
131
+ const parent = resolve(dir, "..");
132
+ if (parent === dir) break;
133
+ dir = parent;
134
+ }
135
+ return startDir;
136
+ }
137
+
138
+ /**
139
+ * Whether the OTel gate should fire for this project. Reads .indusk/config.json
140
+ * and checks otel.role. Returns true if missing/unset/"service", false for
141
+ * library/tool/none. Mirrors shouldEmitOtelGate() in apps/indusk-mcp/src/lib/config.ts.
142
+ */
143
+ function shouldEmitOtelGate(projectRoot) {
144
+ const configPath = `${projectRoot}/.indusk/config.json`;
145
+ if (!existsSync(configPath)) return true;
146
+ try {
147
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
148
+ const role = config?.otel?.role;
149
+ return role === undefined || role === "service";
150
+ } catch {
151
+ return true;
152
+ }
153
+ }
154
+
155
+ const projectRoot = findProjectRoot(event.cwd ?? process.cwd());
156
+ const otelGateEnabled = shouldEmitOtelGate(projectRoot);
157
+
158
+ // Which gate types are required per workflow.
159
+ // OTel is filtered out below when the project opts out via otel.role.
160
+ const WORKFLOW_GATES_BASE = {
120
161
  feature: ["verification", "otel", "context", "document"],
121
162
  refactor: ["verification", "otel", "context", "document"],
122
163
  bugfix: ["verification", "document"],
123
164
  spike: [],
124
165
  };
125
166
 
167
+ const WORKFLOW_GATES = Object.fromEntries(
168
+ Object.entries(WORKFLOW_GATES_BASE).map(([wf, gates]) => [
169
+ wf,
170
+ otelGateEnabled ? gates : gates.filter((g) => g !== "otel"),
171
+ ]),
172
+ );
173
+
126
174
  // Parse phases from the NEW content (after edit) and OLD content (before edit)
127
175
  function parsePhases(content) {
128
176
  // Strip frontmatter
@@ -2,14 +2,20 @@
2
2
  /**
3
3
  * PreToolUse hook: validates that impl phases have all four gate sections.
4
4
  *
5
- * Every phase must have: implementation items, Verification, OTel, Context, Document.
5
+ * Every phase must have: implementation items, Verification, OTel*, Context, Document.
6
6
  * Sections can opt out with (none needed), (not applicable), or skip-reason: {why}.
7
7
  *
8
+ * *OTel section is conditional on the project's `otel.role` in .indusk/config.json:
9
+ * - unset or "service": OTel section is required (default behavior)
10
+ * - "library" / "tool" / "none": OTel section is NOT required (gate silenced)
11
+ * This mirrors `shouldEmitOtelGate()` in apps/indusk-mcp/src/lib/config.ts.
12
+ *
8
13
  * Exit 0 = allow the edit
9
14
  * Exit 2 = block the edit (stderr sent to agent as feedback)
10
15
  */
11
16
 
12
- import { readFileSync } from "node:fs";
17
+ import { existsSync, readFileSync } from "node:fs";
18
+ import { resolve } from "node:path";
13
19
 
14
20
  // Read hook input from stdin
15
21
  let input = "";
@@ -26,6 +32,43 @@ if (!filePath.endsWith("/impl.md") && !filePath.endsWith("\\impl.md")) {
26
32
  process.exit(0);
27
33
  }
28
34
 
35
+ /**
36
+ * Find the project root by walking up from a starting directory looking for
37
+ * a .indusk/ or .claude/ directory. Falls back to event.cwd if none found.
38
+ * Mirrors the pattern used in check-catchup.js.
39
+ */
40
+ function findProjectRoot(startDir) {
41
+ let dir = startDir;
42
+ for (let i = 0; i < 10; i++) {
43
+ if (existsSync(`${dir}/.indusk`) || existsSync(`${dir}/.claude`)) return dir;
44
+ const parent = resolve(dir, "..");
45
+ if (parent === dir) break;
46
+ dir = parent;
47
+ }
48
+ return startDir;
49
+ }
50
+
51
+ /**
52
+ * Whether the OTel gate should fire for this project. Reads .indusk/config.json
53
+ * and checks otel.role. Returns true if the config is missing, if otel.role is
54
+ * unset, or if otel.role is "service" — matches shouldEmitOtelGate() in
55
+ * apps/indusk-mcp/src/lib/config.ts exactly.
56
+ */
57
+ function shouldEmitOtelGate(projectRoot) {
58
+ const configPath = `${projectRoot}/.indusk/config.json`;
59
+ if (!existsSync(configPath)) return true;
60
+ try {
61
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
62
+ const role = config?.otel?.role;
63
+ return role === undefined || role === "service";
64
+ } catch {
65
+ return true; // on parse error, preserve default behavior
66
+ }
67
+ }
68
+
69
+ const projectRoot = findProjectRoot(event.cwd ?? process.cwd());
70
+ const otelGateEnabled = shouldEmitOtelGate(projectRoot);
71
+
29
72
  // Check for skip-gates escape hatch
30
73
  const newContent = toolInput.new_string ?? toolInput.content ?? "";
31
74
 
@@ -100,10 +143,13 @@ const body = fmMatch ? newFullContent.slice(fmMatch[0].length) : newFullContent;
100
143
  const workflowMatch = frontmatter.match(/workflow:\s*(bugfix|refactor|feature|spike)/);
101
144
  const workflow = workflowMatch ? workflowMatch[1] : "feature";
102
145
 
103
- // Different workflows have different requirements
146
+ // Different workflows have different requirements.
147
+ // OTel is further gated on the project's `otel.role` in .indusk/config.json —
148
+ // libraries/tools/none opt out of the OTel gate entirely. Workflows that normally
149
+ // require OTel (feature, refactor) will only require it when otelGateEnabled is true.
104
150
  const requirements = {
105
- feature: { verification: true, otel: true, context: true, document: true },
106
- refactor: { verification: true, otel: true, context: true, document: true },
151
+ feature: { verification: true, otel: otelGateEnabled, context: true, document: true },
152
+ refactor: { verification: true, otel: otelGateEnabled, context: true, document: true },
107
153
  bugfix: { verification: true, otel: false, context: false, document: true },
108
154
  spike: { verification: false, otel: false, context: false, document: false },
109
155
  }[workflow];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infinitedusky/indusk-mcp",
3
- "version": "1.10.0",
3
+ "version": "1.10.1",
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/planner.md CHANGED
@@ -108,6 +108,8 @@ Workflow templates are in `templates/workflows/` in the package. They describe w
108
108
 
109
109
  Default is `ask`. See the work skill "Gate Override Policy" for full details on what each mode enforces at execution time.
110
110
 
111
+ **OTel gate is conditional on `otel.role`.** Read `.indusk/config.json` for the project's `otel.role` field (or use the `shouldEmitOtelGate(projectRoot)` helper from `apps/indusk-mcp/src/lib/config.ts`). The OTel gate fires for projects whose `otel.role` is unset or `"service"` — these are user-facing apps that produce telemetry you want to collect. **Do NOT write `#### Phase N OTel` sections** for projects whose `otel.role` is `"library"`, `"tool"`, or `"none"` — these are libraries, CLIs, or scripts that should never emit telemetry and writing OTel gates for them is friction without value. The `validate-impl-structure` and `check-gates` hooks apply the same rule. The other gates (verify, context, document) always apply regardless of `otel.role`.
112
+
111
113
  7. **If impl is completed** (all items checked off by `/work`), invoke the retrospective skill (`/retrospective {plan-name}`). This handles the structured audit (docs, tests, quality, context), knowledge handoff to the docs site, and archival. Do not write a freeform retrospective — use the skill. (Bugfix and refactor workflows may skip retrospective for small changes — user's call.)
112
114
 
113
115
  8. **Always present each document for review** before moving to the next stage. The user signs off on each step.
@@ -280,6 +282,8 @@ For multi-phase impls, include a boundary map showing what each phase produces a
280
282
  function withdrawFor(wallet: address, player: address, amount: uint256, historyHash: bytes32)
281
283
  ```
282
284
 
285
+ {OPTIONAL: #### Phase 1 OTel — include ONLY if the project's `otel.role` in `.indusk/config.json` is unset or `"service"`. Skip the entire OTel block for projects with `otel.role: "library" | "tool" | "none"`. Use `shouldEmitOtelGate(projectRoot)` from `apps/indusk-mcp/src/lib/config.ts` to decide.}
286
+
283
287
  #### Phase 1 OTel
284
288
  - [ ] {Instrumentation check — are new code paths observable? See the OTel skill for patterns. Example items: "New endpoints have manual spans with `otel.category` and domain attributes", "Errors recorded with `recordException` + `setStatus(ERROR)` + trace-correlated log". Ask: "did this phase add endpoints, business logic, state transitions, or error paths?" If not, this section can be opted out per gate policy.}
285
289