@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 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
- console.info(` ${ext.manifest.name} ${healthStatus}`);
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
  };
@@ -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 !== "strict" &&
221
- (text.includes("(none needed)") ||
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
- : "To skip a gate item, ask the user first, then mark with (none needed) or skip-reason: {why}\n";
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
- if (gatePolicy === "strict") {
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 in strict mode — add real items`,
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
- : "If a section isn't needed, add it with (none needed) or skip-reason: {why}\nExample: #### Phase 1 Document\\n(none needed)\n";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infinitedusky/indusk-mcp",
3
- "version": "1.1.3",
3
+ "version": "1.2.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/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`). See the work skill "Gate Override Policy" for what each mode means. The `validate-impl-structure` hook enforces this at write time.
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
  ```
@@ -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. `(none needed)` and `skip-reason:` are not accepted. Use for critical work where nothing should be skipped. |
73
- | **`ask`** (default) | Agent must ask the user before skipping any gate item. The agent explains why it wants to skip and waits for approval. Only after the user says yes can it mark with `skip-reason:`. |
74
- | **`auto`** | Agent can skip with `skip-reason:` without asking. Use when running autonomously or when you trust the agent's judgment. |
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 mark this as `skip-reason: internal change, no public API change`?"
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 skip-reason and continues
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
- **The agent must NEVER skip a gate without asking in `ask` mode.** This is the core enforcement. The hooks block unauthorized skips, and the skill enforces the conversation.
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).