@infinitedusky/indusk-mcp 1.1.3 → 1.2.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 +7 -0
- package/dist/bin/commands/extensions.d.ts +1 -0
- package/dist/bin/commands/extensions.js +69 -1
- package/dist/lib/extension-loader.d.ts +2 -0
- package/hooks/check-gates.js +21 -5
- package/hooks/validate-impl-structure.js +11 -3
- package/package.json +1 -1
- package/skills/plan.md +21 -1
- package/skills/toolbelt.md +13 -1
- package/skills/work.md +23 -7
package/dist/bin/cli.js
CHANGED
|
@@ -75,6 +75,13 @@ ext
|
|
|
75
75
|
const { extensionsRemove } = await import("./commands/extensions.js");
|
|
76
76
|
await extensionsRemove(process.cwd(), names);
|
|
77
77
|
});
|
|
78
|
+
ext
|
|
79
|
+
.command("update [names...]")
|
|
80
|
+
.description("Update third-party extensions from their original source")
|
|
81
|
+
.action(async (names) => {
|
|
82
|
+
const { extensionsUpdate } = await import("./commands/extensions.js");
|
|
83
|
+
await extensionsUpdate(process.cwd(), names);
|
|
84
|
+
});
|
|
78
85
|
ext
|
|
79
86
|
.command("suggest")
|
|
80
87
|
.description("Recommend extensions based on project contents")
|
|
@@ -4,4 +4,5 @@ export declare function extensionsEnable(projectRoot: string, names: string[]):
|
|
|
4
4
|
export declare function extensionsDisable(projectRoot: string, names: string[]): Promise<void>;
|
|
5
5
|
export declare function extensionsAdd(projectRoot: string, name: string, from: string): Promise<void>;
|
|
6
6
|
export declare function extensionsRemove(projectRoot: string, names: string[]): Promise<void>;
|
|
7
|
+
export declare function extensionsUpdate(projectRoot: string, names?: string[]): Promise<void>;
|
|
7
8
|
export declare function extensionsSuggest(projectRoot: string): Promise<void>;
|
|
@@ -68,7 +68,8 @@ export async function extensionsStatus(projectRoot) {
|
|
|
68
68
|
.map((r) => r.name)
|
|
69
69
|
.join(", ")}`;
|
|
70
70
|
}
|
|
71
|
-
|
|
71
|
+
const source = ext.manifest._source ? ` (from ${ext.manifest._source})` : " (built-in)";
|
|
72
|
+
console.info(` ${ext.manifest.name}${source} — ${healthStatus}`);
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
export async function extensionsEnable(projectRoot, names) {
|
|
@@ -194,9 +195,40 @@ export async function extensionsAdd(projectRoot, name, from) {
|
|
|
194
195
|
console.info(` ${name}: invalid JSON in manifest`);
|
|
195
196
|
return;
|
|
196
197
|
}
|
|
198
|
+
// Store the source in the manifest so `extensions update` can re-fetch
|
|
199
|
+
try {
|
|
200
|
+
const parsed = JSON.parse(manifestContent);
|
|
201
|
+
parsed._source = from;
|
|
202
|
+
manifestContent = JSON.stringify(parsed, null, "\t");
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// leave as-is if parsing fails
|
|
206
|
+
}
|
|
197
207
|
const targetPath = join(extensionsDir(projectRoot), `${name}.json`);
|
|
198
208
|
writeFileSync(targetPath, manifestContent);
|
|
199
209
|
console.info(` ${name}: added from ${from}`);
|
|
210
|
+
// Run post-update hook if defined
|
|
211
|
+
try {
|
|
212
|
+
const manifest = JSON.parse(manifestContent);
|
|
213
|
+
if (manifest.hooks?.on_post_update) {
|
|
214
|
+
console.info(` ${name}: running post-update hook...`);
|
|
215
|
+
try {
|
|
216
|
+
execSync(manifest.hooks.on_post_update, {
|
|
217
|
+
cwd: projectRoot,
|
|
218
|
+
timeout: 30000,
|
|
219
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
220
|
+
});
|
|
221
|
+
console.info(` ${name}: post-update hook completed`);
|
|
222
|
+
}
|
|
223
|
+
catch (e) {
|
|
224
|
+
const err = e;
|
|
225
|
+
console.info(` ${name}: post-update hook failed: ${err.stderr?.trim() ?? "unknown error"}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// ignore parse errors
|
|
231
|
+
}
|
|
200
232
|
}
|
|
201
233
|
export async function extensionsRemove(projectRoot, names) {
|
|
202
234
|
for (const name of names) {
|
|
@@ -221,6 +253,42 @@ export async function extensionsRemove(projectRoot, names) {
|
|
|
221
253
|
}
|
|
222
254
|
}
|
|
223
255
|
}
|
|
256
|
+
export async function extensionsUpdate(projectRoot, names) {
|
|
257
|
+
const extDir = extensionsDir(projectRoot);
|
|
258
|
+
if (!existsSync(extDir)) {
|
|
259
|
+
console.info("No extensions installed.");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
// Find all third-party extensions (ones with _source)
|
|
263
|
+
const files = readdirSync(extDir).filter((f) => f.endsWith(".json"));
|
|
264
|
+
let updated = 0;
|
|
265
|
+
for (const file of files) {
|
|
266
|
+
const name = file.replace(".json", "");
|
|
267
|
+
if (names?.length && !names.includes(name))
|
|
268
|
+
continue;
|
|
269
|
+
try {
|
|
270
|
+
const manifest = JSON.parse(readFileSync(join(extDir, file), "utf-8"));
|
|
271
|
+
if (!manifest._source) {
|
|
272
|
+
if (names && names.includes(name)) {
|
|
273
|
+
console.info(` ${name}: built-in extension — updated via package update, not extensions update`);
|
|
274
|
+
}
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
console.info(` ${name}: updating from ${manifest._source}...`);
|
|
278
|
+
await extensionsAdd(projectRoot, name, manifest._source);
|
|
279
|
+
updated++;
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
console.info(` ${name}: failed to read manifest`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (updated === 0) {
|
|
286
|
+
console.info("No third-party extensions to update.");
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
console.info(`\n${updated} extension(s) updated.`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
224
292
|
export async function extensionsSuggest(projectRoot) {
|
|
225
293
|
const builtins = getBuiltinExtensions();
|
|
226
294
|
const suggestions = [];
|
|
@@ -17,6 +17,7 @@ export interface ExtensionManifest {
|
|
|
17
17
|
name: string;
|
|
18
18
|
description: string;
|
|
19
19
|
version?: string;
|
|
20
|
+
_source?: string;
|
|
20
21
|
provides: {
|
|
21
22
|
skill?: boolean;
|
|
22
23
|
networking?: {
|
|
@@ -38,6 +39,7 @@ export interface ExtensionManifest {
|
|
|
38
39
|
hooks?: {
|
|
39
40
|
on_init?: string;
|
|
40
41
|
on_update?: string;
|
|
42
|
+
on_post_update?: string;
|
|
41
43
|
on_health_check?: string;
|
|
42
44
|
on_onboard?: string;
|
|
43
45
|
};
|
package/hooks/check-gates.js
CHANGED
|
@@ -216,11 +216,25 @@ for (const item of newlyChecked) {
|
|
|
216
216
|
for (const phase of oldPhases) {
|
|
217
217
|
if (phase.number >= item.phase) break;
|
|
218
218
|
|
|
219
|
-
const isOverridden = (text) =>
|
|
220
|
-
gatePolicy
|
|
221
|
-
|
|
219
|
+
const isOverridden = (text) => {
|
|
220
|
+
if (gatePolicy === "strict") return false;
|
|
221
|
+
|
|
222
|
+
const hasBareOptOut =
|
|
223
|
+
text.includes("(none needed)") ||
|
|
222
224
|
text.includes("(not applicable)") ||
|
|
223
|
-
text.includes("skip-reason:")
|
|
225
|
+
text.includes("skip-reason:");
|
|
226
|
+
|
|
227
|
+
if (gatePolicy === "auto") return hasBareOptOut;
|
|
228
|
+
|
|
229
|
+
// ask mode: requires conversation proof
|
|
230
|
+
// Format: (none needed — asked: "{question}" — user: "{answer}")
|
|
231
|
+
const hasConversationProof =
|
|
232
|
+
/\(none needed\s*—\s*asked:\s*"[^"]+"\s*—\s*user:\s*"[^"]+"\)/.test(text) ||
|
|
233
|
+
/\(not applicable\s*—\s*asked:\s*"[^"]+"\s*—\s*user:\s*"[^"]+"\)/.test(text) ||
|
|
234
|
+
/skip-reason:.*—\s*asked:\s*"[^"]+"\s*—\s*user:\s*"[^"]+"/.test(text);
|
|
235
|
+
|
|
236
|
+
return hasConversationProof;
|
|
237
|
+
};
|
|
224
238
|
|
|
225
239
|
const uncheckedGates = phase.items.filter(
|
|
226
240
|
(i) => !i.checked && !isOverridden(i.text) && requiredGates.includes(i.gate),
|
|
@@ -231,7 +245,9 @@ for (const item of newlyChecked) {
|
|
|
231
245
|
const skipHint =
|
|
232
246
|
gatePolicy === "strict"
|
|
233
247
|
? "Gate policy is 'strict' — no overrides allowed.\n"
|
|
234
|
-
:
|
|
248
|
+
: gatePolicy === "ask"
|
|
249
|
+
? 'Gate policy is \'ask\' — to skip, you must ask the user and include proof.\nFormat: (none needed — asked: "your question" — user: "their answer")\n'
|
|
250
|
+
: "To skip a gate item, mark with (none needed) or skip-reason: {why}\n";
|
|
235
251
|
process.stderr.write(
|
|
236
252
|
`Phase ${item.phase} blocked (policy: ${gatePolicy}): complete Phase ${phase.number} gates first:\n${missing}\n${skipHint}`,
|
|
237
253
|
);
|
|
@@ -194,7 +194,9 @@ for (const phase of phases) {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
// In strict mode, opt-outs are not allowed — sections must have real items
|
|
197
|
-
|
|
197
|
+
// In ask mode, opt-outs are not allowed at write time — every gate must have a real item
|
|
198
|
+
// Opt-outs only happen during /work (execution time), not during /plan (write time)
|
|
199
|
+
if (gatePolicy === "strict" || gatePolicy === "ask") {
|
|
198
200
|
const optOuts = [];
|
|
199
201
|
if (requirements.verification && phase.hasVerification && phase.verificationIsOptOut)
|
|
200
202
|
optOuts.push("Verification");
|
|
@@ -202,8 +204,12 @@ for (const phase of phases) {
|
|
|
202
204
|
if (requirements.document && phase.hasDocument && phase.documentIsOptOut)
|
|
203
205
|
optOuts.push("Document");
|
|
204
206
|
if (optOuts.length > 0) {
|
|
207
|
+
const modeHint =
|
|
208
|
+
gatePolicy === "strict"
|
|
209
|
+
? "strict mode — no opt-outs allowed"
|
|
210
|
+
: "ask mode — every gate must have a real item when the impl is written. Opt-outs happen during /work after asking the user";
|
|
205
211
|
errors.push(
|
|
206
|
-
`Phase ${phase.number} (${phase.name}): ${optOuts.join(", ")} cannot use opt-outs
|
|
212
|
+
`Phase ${phase.number} (${phase.name}): ${optOuts.join(", ")} cannot use opt-outs at write time (${modeHint})`,
|
|
207
213
|
);
|
|
208
214
|
}
|
|
209
215
|
}
|
|
@@ -217,7 +223,9 @@ if (errors.length > 0) {
|
|
|
217
223
|
const skipHint =
|
|
218
224
|
gatePolicy === "strict"
|
|
219
225
|
? "Gate policy is 'strict' — all sections must have real items, no overrides.\n"
|
|
220
|
-
:
|
|
226
|
+
: gatePolicy === "ask"
|
|
227
|
+
? "Gate policy is 'ask' — every gate must have a real item when writing the impl. Opt-outs happen during /work after asking the user.\n"
|
|
228
|
+
: "If a section isn't needed, add it with (none needed) or skip-reason: {why}\nExample: #### Phase 1 Document\\n(none needed)\n";
|
|
221
229
|
process.stderr.write(
|
|
222
230
|
`Impl structure incomplete (workflow: ${workflow}, policy: ${gatePolicy}):\n${msg}\n\nThis workflow requires: ${reqNames.join(", ")} sections per phase.\n${skipHint}To change requirements, add 'workflow: bugfix' to the impl frontmatter.\n`,
|
|
223
231
|
);
|
package/package.json
CHANGED
package/skills/plan.md
CHANGED
|
@@ -78,7 +78,11 @@ Workflow templates are in `templates/workflows/` in the package. They describe w
|
|
|
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
|
|
|
81
|
-
**Gate policy applies when writing impls.** Set `gate_policy` in the impl frontmatter (`strict`, `ask`, or `auto`).
|
|
81
|
+
**Gate policy applies when writing impls.** Set `gate_policy` in the impl frontmatter (`strict`, `ask`, or `auto`). The `validate-impl-structure` hook enforces this at write time:
|
|
82
|
+
- **`strict` / `ask`**: Every gate section (Verification, Context, Document) must have a real item — `(none needed)` and `skip-reason:` are blocked at write time. Opt-outs only happen during `/work` execution.
|
|
83
|
+
- **`auto`**: Gate sections can be pre-filled with `(none needed)` or `skip-reason:` at write time.
|
|
84
|
+
|
|
85
|
+
Default is `ask`. See the work skill "Gate Override Policy" for full details on what each mode enforces at execution time.
|
|
82
86
|
|
|
83
87
|
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.)
|
|
84
88
|
|
|
@@ -193,6 +197,22 @@ because **{rationale}**.
|
|
|
193
197
|
### Risks
|
|
194
198
|
- {Risk and mitigation}
|
|
195
199
|
|
|
200
|
+
## Documentation Plan
|
|
201
|
+
{Decide upfront what documentation this feature produces. This shapes the Document gates in the impl.}
|
|
202
|
+
|
|
203
|
+
### Pages
|
|
204
|
+
- {New page or existing page to update — e.g., "New: reference/tools/settlement-api.md", "Update: guide/getting-started.md"}
|
|
205
|
+
|
|
206
|
+
### Diagrams
|
|
207
|
+
- {What diagrams are needed — e.g., "Architecture diagram showing settlement flow", "Sequence diagram for agent registration"}
|
|
208
|
+
- {Where they go — e.g., "Mermaid in reference/tools/settlement-api.md", "Standalone in guide/architecture.md"}
|
|
209
|
+
|
|
210
|
+
### Changelog
|
|
211
|
+
- {What changelog entry — e.g., "Added settlement API with EIP-712 receipts"}
|
|
212
|
+
|
|
213
|
+
### ADR in Docs
|
|
214
|
+
- {Should this ADR be published to the docs site? If yes, which section — e.g., "decisions/settlement-architecture.md"}
|
|
215
|
+
|
|
196
216
|
## References
|
|
197
217
|
- {Links to research, brief, related plans, external resources}
|
|
198
218
|
```
|
package/skills/toolbelt.md
CHANGED
|
@@ -10,7 +10,7 @@ You have MCP tools from two servers: **indusk** (dev system) and **codegraphcont
|
|
|
10
10
|
/work → executes impl checklist, phase by phase
|
|
11
11
|
each phase has four gates:
|
|
12
12
|
implement → verify → context → document → next phase
|
|
13
|
-
hooks enforce gates — can't skip
|
|
13
|
+
hooks enforce gates — can't skip (see Gate Policy below)
|
|
14
14
|
↓
|
|
15
15
|
/retrospective → audit, quality ratchet, knowledge handoff, archive
|
|
16
16
|
```
|
|
@@ -52,6 +52,18 @@ While executing impl items:
|
|
|
52
52
|
- After completing context items, call `get_context` to verify CLAUDE.md was updated correctly.
|
|
53
53
|
- After completing document items, call `list_docs` to verify the doc page exists.
|
|
54
54
|
|
|
55
|
+
## Gate Policy
|
|
56
|
+
|
|
57
|
+
Gates prevent skipping important work. Three enforcement levels, set via `gate_policy` in impl frontmatter or `.claude/settings.json`:
|
|
58
|
+
|
|
59
|
+
| Mode | Writing the impl (`/plan`) | Executing the impl (`/work`) |
|
|
60
|
+
|------|---------------------------|------------------------------|
|
|
61
|
+
| **`strict`** | Every gate must have a real item. No `(none needed)`. | Every item must be completed. No skipping. |
|
|
62
|
+
| **`ask`** (default) | Every gate must have a real item. No `(none needed)`. | Skip only with conversation proof: `(none needed — asked: "..." — user: "...")` |
|
|
63
|
+
| **`auto`** | `(none needed)` / `skip-reason:` allowed at write time. | Skip without asking using `(none needed)` or `skip-reason:`. |
|
|
64
|
+
|
|
65
|
+
Hooks enforce both stages. See the work skill "Gate Override Policy" for full details.
|
|
66
|
+
|
|
55
67
|
## Advancing Phases
|
|
56
68
|
|
|
57
69
|
When you think a phase is complete:
|
package/skills/work.md
CHANGED
|
@@ -69,9 +69,9 @@ Three modes, configured via `gate_policy` in the impl frontmatter or `.claude/se
|
|
|
69
69
|
|
|
70
70
|
| Mode | Behavior |
|
|
71
71
|
|------|----------|
|
|
72
|
-
| **`strict`** | No overrides. Every gate item must be completed
|
|
73
|
-
| **`ask`** (default) |
|
|
74
|
-
| **`auto`** |
|
|
72
|
+
| **`strict`** | No overrides at any stage. Every gate must have a real item when the impl is written (`/plan`), and every item must be completed during `/work`. No `(none needed)`, no `skip-reason:`, no conversation proof. |
|
|
73
|
+
| **`ask`** (default) | Every gate must have a real item when the impl is written. During `/work`, the agent must ask the user before skipping, and include proof of the conversation in the skip format. Hooks enforce both stages. |
|
|
74
|
+
| **`auto`** | Gates can be pre-filled with `(none needed)` or `skip-reason:` at write time. During `/work`, the agent can skip without asking. Use when running autonomously. |
|
|
75
75
|
|
|
76
76
|
### How to set the mode
|
|
77
77
|
|
|
@@ -100,14 +100,30 @@ Priority: per-invocation > per-plan > per-project > default (`ask`).
|
|
|
100
100
|
|
|
101
101
|
When the agent encounters a gate item it thinks should be skipped:
|
|
102
102
|
|
|
103
|
-
> "Phase 2 has a Document gate: 'Write reference page for the new API.' I don't think this phase needs a new docs page because we only changed internal implementation — the public API didn't change. Can I
|
|
103
|
+
> "Phase 2 has a Document gate: 'Write reference page for the new API.' I don't think this phase needs a new docs page because we only changed internal implementation — the public API didn't change. Can I skip the document gate?"
|
|
104
104
|
|
|
105
105
|
The user can say:
|
|
106
|
-
- **"yes"** — agent marks it with
|
|
106
|
+
- **"yes, skip it"** — agent marks it with conversation proof and continues
|
|
107
107
|
- **"no, do it"** — agent completes the gate item
|
|
108
|
-
- **"no, but mark it (none needed)"** — if the gate truly doesn't apply
|
|
109
108
|
|
|
110
|
-
|
|
109
|
+
### Conversation proof format (enforced by hooks)
|
|
110
|
+
|
|
111
|
+
In `ask` mode, skipped gates MUST include proof that the conversation happened:
|
|
112
|
+
|
|
113
|
+
```markdown
|
|
114
|
+
#### Phase 2 Document
|
|
115
|
+
- [x] (none needed — asked: "Phase 2 is internal refactoring with no public API changes. Can I skip the document gate?" — user: "yes, skip it")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The hook validates that both `asked:` and `user:` are present with non-empty quoted content. Bare `(none needed)` or `skip-reason:` without conversation proof will be **blocked by the hook**.
|
|
119
|
+
|
|
120
|
+
| Mode | At write time (`/plan`) | At execution time (`/work`) |
|
|
121
|
+
|------|------------------------|---------------------------|
|
|
122
|
+
| `strict` | No opt-outs — real items required | No skipping — everything completed |
|
|
123
|
+
| `ask` | No opt-outs — real items required | Skip only with conversation proof |
|
|
124
|
+
| `auto` | `(none needed)` / `skip-reason:` allowed | Skip without asking |
|
|
125
|
+
|
|
126
|
+
**The agent must NEVER skip a gate without asking in `ask` mode.** This is enforced by hooks at both stages — not just instructional.
|
|
111
127
|
|
|
112
128
|
11. **Verification items.** The Verification section requires proof, not assumption. See the verify skill for full guidance.
|
|
113
129
|
- Run checks in order: type check → lint → affected tests → build. Skip checks that don't apply (see verify skill's skip logic table).
|