@infinitedusky/indusk-mcp 0.7.2 → 0.8.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/hooks/check-gates.js +25 -3
- package/hooks/validate-impl-structure.js +170 -0
- package/package.json +1 -1
- package/skills/document.md +30 -1
- package/skills/plan.md +9 -2
package/hooks/check-gates.js
CHANGED
|
@@ -80,6 +80,22 @@ if (event.tool_name === "Edit" && oldContent) {
|
|
|
80
80
|
process.exit(0);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// Detect workflow type from content frontmatter
|
|
84
|
+
function detectWorkflow(content) {
|
|
85
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n/);
|
|
86
|
+
const fm = fmMatch ? fmMatch[1] : "";
|
|
87
|
+
const m = fm.match(/workflow:\s*(bugfix|refactor|feature|spike)/);
|
|
88
|
+
return m ? m[1] : "feature";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Which gate types are required per workflow
|
|
92
|
+
const WORKFLOW_GATES = {
|
|
93
|
+
feature: ["verification", "context", "document"],
|
|
94
|
+
refactor: ["verification", "context", "document"],
|
|
95
|
+
bugfix: ["verification", "document"],
|
|
96
|
+
spike: [],
|
|
97
|
+
};
|
|
98
|
+
|
|
83
99
|
// Parse phases from the NEW content (after edit) and OLD content (before edit)
|
|
84
100
|
function parsePhases(content) {
|
|
85
101
|
// Strip frontmatter
|
|
@@ -131,6 +147,8 @@ function parsePhases(content) {
|
|
|
131
147
|
return phases;
|
|
132
148
|
}
|
|
133
149
|
|
|
150
|
+
const workflow = detectWorkflow(fullContent);
|
|
151
|
+
const requiredGates = WORKFLOW_GATES[workflow] || WORKFLOW_GATES.feature;
|
|
134
152
|
const oldPhases = parsePhases(fullContent);
|
|
135
153
|
const newPhases = parsePhases(newFullContent);
|
|
136
154
|
|
|
@@ -171,15 +189,19 @@ for (const item of newlyChecked) {
|
|
|
171
189
|
for (const phase of oldPhases) {
|
|
172
190
|
if (phase.number >= item.phase) break;
|
|
173
191
|
|
|
192
|
+
const isOverridden = (text) =>
|
|
193
|
+
text.includes("(none needed)") ||
|
|
194
|
+
text.includes("(not applicable)") ||
|
|
195
|
+
text.includes("skip-reason:");
|
|
196
|
+
|
|
174
197
|
const uncheckedGates = phase.items.filter(
|
|
175
|
-
(i) =>
|
|
176
|
-
!i.checked && (i.gate === "verification" || i.gate === "context" || i.gate === "document"),
|
|
198
|
+
(i) => !i.checked && !isOverridden(i.text) && requiredGates.includes(i.gate),
|
|
177
199
|
);
|
|
178
200
|
|
|
179
201
|
if (uncheckedGates.length > 0) {
|
|
180
202
|
const missing = uncheckedGates.map((i) => ` [${i.gate}] ${i.text}`).join("\n");
|
|
181
203
|
process.stderr.write(
|
|
182
|
-
`Phase ${item.phase} blocked: complete Phase ${phase.number} gates first:\n${missing}\n`,
|
|
204
|
+
`Phase ${item.phase} blocked: complete Phase ${phase.number} gates first:\n${missing}\nTo skip a gate item, mark it with (none needed) or skip-reason: {why}\n`,
|
|
183
205
|
);
|
|
184
206
|
process.exit(2);
|
|
185
207
|
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PreToolUse hook: validates that impl phases have all four gate sections.
|
|
4
|
+
*
|
|
5
|
+
* Every phase must have: implementation items, Verification, Context, Document.
|
|
6
|
+
* Sections can opt out with (none needed), (not applicable), or skip-reason: {why}.
|
|
7
|
+
*
|
|
8
|
+
* Exit 0 = allow the edit
|
|
9
|
+
* Exit 2 = block the edit (stderr sent to agent as feedback)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
|
|
14
|
+
// Read hook input from stdin
|
|
15
|
+
let input = "";
|
|
16
|
+
for await (const chunk of process.stdin) {
|
|
17
|
+
input += chunk;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const event = JSON.parse(input);
|
|
21
|
+
const toolInput = event.tool_input ?? {};
|
|
22
|
+
const filePath = toolInput.file_path ?? "";
|
|
23
|
+
|
|
24
|
+
// Fast path: not an impl.md file
|
|
25
|
+
if (!filePath.endsWith("/impl.md") && !filePath.endsWith("\\impl.md")) {
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check for skip-gates escape hatch
|
|
30
|
+
const newContent = toolInput.new_string ?? toolInput.content ?? "";
|
|
31
|
+
if (newContent.includes("<!-- skip-gates -->")) {
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Determine the full new content after edit
|
|
36
|
+
let newFullContent;
|
|
37
|
+
if (event.tool_name === "Edit" && toolInput.old_string) {
|
|
38
|
+
try {
|
|
39
|
+
const diskContent = readFileSync(filePath, "utf-8");
|
|
40
|
+
newFullContent = diskContent.replace(toolInput.old_string, newContent);
|
|
41
|
+
} catch {
|
|
42
|
+
// File doesn't exist yet — will be created by Write
|
|
43
|
+
newFullContent = newContent;
|
|
44
|
+
}
|
|
45
|
+
} else if (event.tool_name === "Write") {
|
|
46
|
+
newFullContent = toolInput.content ?? "";
|
|
47
|
+
} else {
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Only validate if this edit is adding/modifying phase structure
|
|
52
|
+
// Check if the edit contains phase headers
|
|
53
|
+
const editContent = toolInput.new_string ?? toolInput.content ?? "";
|
|
54
|
+
const hasPhaseHeader = /###\s+Phase\s+\d+/.test(editContent);
|
|
55
|
+
const hasChecklistItem = /- \[ \]/.test(editContent);
|
|
56
|
+
|
|
57
|
+
// If the edit doesn't touch phase structure, allow it
|
|
58
|
+
if (!hasPhaseHeader && !hasChecklistItem) {
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Parse frontmatter to detect workflow type
|
|
63
|
+
const fmMatch = newFullContent.match(/^---\n([\s\S]*?)\n---\n/);
|
|
64
|
+
const frontmatter = fmMatch ? fmMatch[1] : "";
|
|
65
|
+
const body = fmMatch ? newFullContent.slice(fmMatch[0].length) : newFullContent;
|
|
66
|
+
|
|
67
|
+
// Detect workflow type from frontmatter (workflow: bugfix|refactor|feature)
|
|
68
|
+
// or infer from plan structure
|
|
69
|
+
const workflowMatch = frontmatter.match(/workflow:\s*(bugfix|refactor|feature|spike)/);
|
|
70
|
+
const workflow = workflowMatch ? workflowMatch[1] : "feature";
|
|
71
|
+
|
|
72
|
+
// Different workflows have different requirements
|
|
73
|
+
const requirements = {
|
|
74
|
+
feature: { verification: true, context: true, document: true },
|
|
75
|
+
refactor: { verification: true, context: true, document: true },
|
|
76
|
+
bugfix: { verification: true, context: false, document: true },
|
|
77
|
+
spike: { verification: false, context: false, document: false },
|
|
78
|
+
}[workflow];
|
|
79
|
+
const lines = body.split("\n");
|
|
80
|
+
|
|
81
|
+
const phases = [];
|
|
82
|
+
let currentPhase = null;
|
|
83
|
+
let currentSection = "implementation";
|
|
84
|
+
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
const phaseMatch = line.match(/^###\s+Phase\s+(\d+)[:\s]+(.*)/);
|
|
87
|
+
if (phaseMatch) {
|
|
88
|
+
if (currentPhase) phases.push(currentPhase);
|
|
89
|
+
currentPhase = {
|
|
90
|
+
number: parseInt(phaseMatch[1], 10),
|
|
91
|
+
name: phaseMatch[2].trim(),
|
|
92
|
+
hasImplementation: false,
|
|
93
|
+
hasVerification: false,
|
|
94
|
+
hasContext: false,
|
|
95
|
+
hasDocument: false,
|
|
96
|
+
};
|
|
97
|
+
currentSection = "implementation";
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!currentPhase) continue;
|
|
102
|
+
|
|
103
|
+
// Detect gate section headers
|
|
104
|
+
const verMatch = line.match(/^####\s+Phase\s+\d+\s+Verification\b/);
|
|
105
|
+
if (verMatch) {
|
|
106
|
+
currentPhase.hasVerification = true;
|
|
107
|
+
currentSection = "verification";
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const ctxMatch = line.match(/^####\s+Phase\s+\d+\s+Context\b/);
|
|
112
|
+
if (ctxMatch) {
|
|
113
|
+
currentPhase.hasContext = true;
|
|
114
|
+
currentSection = "context";
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const docMatch = line.match(/^####\s+Phase\s+\d+\s+Document\b/);
|
|
119
|
+
if (docMatch) {
|
|
120
|
+
currentPhase.hasDocument = true;
|
|
121
|
+
currentSection = "document";
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Check for implementation items
|
|
126
|
+
if (currentSection === "implementation" && /^-\s+\[[ x]\]/.test(line)) {
|
|
127
|
+
currentPhase.hasImplementation = true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Forward intelligence doesn't count as a gate
|
|
131
|
+
if (line.match(/^####\s+Phase\s+\d+\s+Forward Intelligence\b/)) {
|
|
132
|
+
currentSection = "fi";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (currentPhase) phases.push(currentPhase);
|
|
136
|
+
|
|
137
|
+
// Check for opt-out content in gate sections
|
|
138
|
+
// Re-scan to check if sections that exist have (none needed) or skip-reason:
|
|
139
|
+
const isOptedOut = (text) =>
|
|
140
|
+
text.includes("(none needed)") ||
|
|
141
|
+
text.includes("(not applicable)") ||
|
|
142
|
+
text.includes("skip-reason:");
|
|
143
|
+
|
|
144
|
+
// Validate each phase
|
|
145
|
+
const errors = [];
|
|
146
|
+
for (const phase of phases) {
|
|
147
|
+
if (!phase.hasImplementation) continue; // Skip phases with no impl items (might be a header-only outline)
|
|
148
|
+
|
|
149
|
+
const missing = [];
|
|
150
|
+
if (requirements.verification && !phase.hasVerification) missing.push("Verification");
|
|
151
|
+
if (requirements.context && !phase.hasContext) missing.push("Context");
|
|
152
|
+
if (requirements.document && !phase.hasDocument) missing.push("Document");
|
|
153
|
+
|
|
154
|
+
if (missing.length > 0) {
|
|
155
|
+
errors.push(`Phase ${phase.number} (${phase.name}) is missing: ${missing.join(", ")}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (errors.length > 0) {
|
|
160
|
+
const msg = errors.join("\n");
|
|
161
|
+
const reqNames = Object.entries(requirements)
|
|
162
|
+
.filter(([, v]) => v)
|
|
163
|
+
.map(([k]) => k.charAt(0).toUpperCase() + k.slice(1));
|
|
164
|
+
process.stderr.write(
|
|
165
|
+
`Impl structure incomplete (workflow: ${workflow}):\n${msg}\n\nThis workflow requires: ${reqNames.join(", ")} sections per phase.\nIf a section isn't needed, add it with (none needed) or skip-reason: {why}\nExample: #### Phase 1 Document\\n(none needed)\nTo change requirements, add 'workflow: bugfix' to the impl frontmatter.\n`,
|
|
166
|
+
);
|
|
167
|
+
process.exit(2);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
process.exit(0);
|
package/package.json
CHANGED
package/skills/document.md
CHANGED
|
@@ -97,6 +97,35 @@ flowchart TD
|
|
|
97
97
|
|
|
98
98
|
**Never** use bare ` ```mermaid ` blocks without the wrapper. The diagrams are often too small to read inline, and the FullscreenDiagram gives users zoom and pan controls.
|
|
99
99
|
|
|
100
|
+
## Documentation by Workflow Type
|
|
101
|
+
|
|
102
|
+
The document gate's expectations vary by workflow type. The hook enforces that the section exists, but what goes in it depends on what you're building.
|
|
103
|
+
|
|
104
|
+
### Feature (full documentation)
|
|
105
|
+
|
|
106
|
+
A feature is new functionality. It needs complete documentation:
|
|
107
|
+
- Write new reference pages for tools, APIs, or configuration added
|
|
108
|
+
- Write new guide pages for workflows introduced
|
|
109
|
+
- Create Mermaid diagrams for architecture, flows, and relationships
|
|
110
|
+
- Update existing pages that reference the area you changed
|
|
111
|
+
- Create FullscreenDiagram-wrapped diagrams for anything structural
|
|
112
|
+
|
|
113
|
+
### Refactor (update existing docs)
|
|
114
|
+
|
|
115
|
+
A refactor restructures existing code. Documentation should reflect the new structure:
|
|
116
|
+
- Update existing pages that reference moved/renamed code
|
|
117
|
+
- Update diagrams that show the old structure
|
|
118
|
+
- Don't create new pages unless the refactor introduces new concepts
|
|
119
|
+
- If nothing user-facing changed, mark with `(none needed)`
|
|
120
|
+
|
|
121
|
+
### Bugfix (document the fix)
|
|
122
|
+
|
|
123
|
+
A bugfix solves a specific problem. Documentation is about the fix, not new content:
|
|
124
|
+
- Update any docs that described the broken behavior
|
|
125
|
+
- If the bug revealed a gotcha, add it to the relevant reference page
|
|
126
|
+
- If the fix changes a public API or configuration, update that reference page
|
|
127
|
+
- For internal-only fixes, mark with `(none needed)`
|
|
128
|
+
|
|
100
129
|
## Shaping Impl Documents
|
|
101
130
|
|
|
102
131
|
When writing an impl (via the plan skill), every phase should consider documentation:
|
|
@@ -106,7 +135,7 @@ When writing an impl (via the plan skill), every phase should consider documenta
|
|
|
106
135
|
- [ ] {Specific docs page to write or update}
|
|
107
136
|
```
|
|
108
137
|
|
|
109
|
-
The agent writing the impl must answer: **"What does a user or developer need to know about what this phase built?"** If the answer is "nothing user-facing" —
|
|
138
|
+
The agent writing the impl must answer: **"What does a user or developer need to know about what this phase built?"** If the answer is "nothing user-facing" — `(none needed)` or `skip-reason:` to opt out. Not every phase produces docs. But the question must be asked, and the section must exist (enforced by hook).
|
|
110
139
|
|
|
111
140
|
### Document Items Are Blocking
|
|
112
141
|
|
package/skills/plan.md
CHANGED
|
@@ -49,7 +49,14 @@ Workflow templates are in `templates/workflows/` in the package. They describe w
|
|
|
49
49
|
|
|
50
50
|
2. **Figure out where things stand.** If a plan folder already exists, read what's there. Check frontmatter statuses. The next document to write is the first one that's missing or incomplete.
|
|
51
51
|
|
|
52
|
-
3. **If starting fresh**,
|
|
52
|
+
3. **If starting fresh**, do a quick scan of the project (read CLAUDE.md, check the code graph) to understand the context. Then **ask the user discovery questions before doing any research or writing any documents.** The goal is to understand what they're trying to achieve, not just what they named the plan. Good discovery questions:
|
|
53
|
+
- "What problem are you trying to solve?" or "What should this feature do for your users?"
|
|
54
|
+
- "Is there anything specific you've already thought through or have strong opinions about?"
|
|
55
|
+
- "Are there any constraints I should know about — timeline, technology preferences, things to avoid?"
|
|
56
|
+
|
|
57
|
+
For non-developers especially, this conversation is critical. They may not know the right technical terms, but they know what they want. Draw that out before proceeding.
|
|
58
|
+
|
|
59
|
+
Once you understand the intent, create the plan folder and start with the first document for the workflow type:
|
|
53
60
|
- **feature**: start with research
|
|
54
61
|
- **bugfix**: start with brief (streamlined template)
|
|
55
62
|
- **refactor**: start with brief (includes boundary map)
|
|
@@ -62,7 +69,7 @@ Workflow templates are in `templates/workflows/` in the package. They describe w
|
|
|
62
69
|
- Include the graph findings in research.md — concrete numbers like "X has 12 dependents across 3 apps"
|
|
63
70
|
Document what you find. The research doc records findings and analysis, but saves the recommendation for the brief.
|
|
64
71
|
|
|
65
|
-
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. Present
|
|
72
|
+
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. **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`.
|
|
66
73
|
|
|
67
74
|
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`
|
|
68
75
|
|