@infinitedusky/indusk-mcp 1.1.2 → 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 = [];
@@ -176,7 +176,12 @@ export async function init(projectRoot, options = {}) {
176
176
  console.info("\n[Hooks]");
177
177
  const hooksSource = join(packageRoot, "hooks");
178
178
  const hooksTarget = join(projectRoot, ".claude/hooks");
179
- const hookFiles = ["check-gates.js", "gate-reminder.js", "validate-impl-structure.js", "check-catchup.js"];
179
+ const hookFiles = [
180
+ "check-gates.js",
181
+ "gate-reminder.js",
182
+ "validate-impl-structure.js",
183
+ "check-catchup.js",
184
+ ];
180
185
  if (existsSync(hooksSource)) {
181
186
  mkdirSync(hooksTarget, { recursive: true });
182
187
  for (const file of hookFiles) {
@@ -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
  };
@@ -6,22 +6,58 @@ function cgcPath() {
6
6
  const paths = [join(process.env.HOME ?? "", ".local/bin/cgc"), "/usr/local/bin/cgc"];
7
7
  return paths.find((p) => existsSync(p)) ?? null;
8
8
  }
9
- function runCgc(args, projectRoot) {
9
+ function getFalkorHost() {
10
+ if (process.env.FALKORDB_HOST)
11
+ return process.env.FALKORDB_HOST;
12
+ // Try OrbStack hostname first, fall back to localhost
13
+ try {
14
+ execSync("ping -c 1 -W 1 falkordb.orb.local", {
15
+ stdio: ["ignore", "ignore", "ignore"],
16
+ timeout: 2000,
17
+ });
18
+ return "falkordb.orb.local";
19
+ }
20
+ catch {
21
+ return "localhost";
22
+ }
23
+ }
24
+ function checkFalkorConnection(host) {
25
+ try {
26
+ // Fast TCP check — try to connect to Redis port
27
+ execSync(`node -e "const s=require('net').connect(6379,'${host}');s.setTimeout(2000);s.on('connect',()=>{s.end();process.exit(0)});s.on('error',()=>process.exit(1));s.on('timeout',()=>process.exit(1))"`, { timeout: 3000, stdio: ["ignore", "ignore", "ignore"] });
28
+ return true;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ function runCgc(args, projectRoot, options) {
10
35
  const cgc = cgcPath();
11
36
  if (!cgc) {
12
37
  return JSON.stringify({ error: "CGC not installed — run: pipx install codegraphcontext" });
13
38
  }
39
+ const host = getFalkorHost();
40
+ const timeout = options?.timeout ?? 15000;
41
+ // Fast pre-check: is FalkorDB reachable?
42
+ if (!options?.skipConnectionCheck && !checkFalkorConnection(host)) {
43
+ return JSON.stringify({
44
+ error: `FalkorDB not reachable at ${host}:6379. Is the container running? Try: docker start falkordb`,
45
+ host,
46
+ suggestion: host === "localhost"
47
+ ? "OrbStack not detected. If using OrbStack, ensure it's running."
48
+ : "Try: docker start falkordb — or check if OrbStack is running.",
49
+ });
50
+ }
14
51
  try {
15
52
  return execSync(`${cgc} ${args}`, {
16
53
  encoding: "utf-8",
17
- timeout: 60000,
54
+ timeout,
18
55
  stdio: ["ignore", "pipe", "pipe"],
19
56
  cwd: projectRoot,
20
57
  env: {
21
58
  ...process.env,
22
59
  DATABASE_TYPE: "falkordb-remote",
23
- FALKORDB_HOST: process.env.FALKORDB_HOST ?? "falkordb.orb.local",
24
- FALKORDB_PORT: process.env.FALKORDB_PORT ?? "6379",
60
+ FALKORDB_HOST: host,
25
61
  FALKORDB_GRAPH_NAME: process.env.FALKORDB_GRAPH_NAME ?? basename(projectRoot),
26
62
  },
27
63
  }).trim();
@@ -38,10 +74,16 @@ export function indexProject(projectRoot) {
38
74
  if (!cgc) {
39
75
  return { success: false, output: "CGC not installed — run: pipx install codegraphcontext" };
40
76
  }
77
+ const host = getFalkorHost();
78
+ if (!checkFalkorConnection(host)) {
79
+ return {
80
+ success: false,
81
+ output: `FalkorDB not reachable at ${host}:6379. Is the container running? Try: docker start falkordb`,
82
+ };
83
+ }
41
84
  const graphName = process.env.FALKORDB_GRAPH_NAME ?? basename(projectRoot);
85
+ const hasIgnore = existsSync(join(projectRoot, ".cgcignore"));
42
86
  try {
43
- // Check if .cgcignore exists
44
- const hasIgnore = existsSync(join(projectRoot, ".cgcignore"));
45
87
  const output = execSync(`${cgc} index ${projectRoot}`, {
46
88
  encoding: "utf-8",
47
89
  timeout: 120000,
@@ -49,8 +91,7 @@ export function indexProject(projectRoot) {
49
91
  env: {
50
92
  ...process.env,
51
93
  DATABASE_TYPE: "falkordb-remote",
52
- FALKORDB_HOST: process.env.FALKORDB_HOST ?? "falkordb.orb.local",
53
- FALKORDB_PORT: process.env.FALKORDB_PORT ?? "6379",
94
+ FALKORDB_HOST: host,
54
95
  FALKORDB_GRAPH_NAME: graphName,
55
96
  },
56
97
  }).trim();
@@ -268,4 +309,141 @@ export function registerGraphTools(server, projectRoot) {
268
309
  content: [{ type: "text", text: output }],
269
310
  };
270
311
  });
312
+ server.registerTool("graph_ensure", {
313
+ description: "Validate and fix the entire code graph stack: FalkorDB container, CGC connection, repo indexing. Call this during catchup or when graph tools fail. Attempts auto-repair for common issues.",
314
+ }, async () => {
315
+ const steps = [];
316
+ // 1. Check CGC installed
317
+ const cgc = cgcPath();
318
+ if (!cgc) {
319
+ steps.push({
320
+ step: "cgc-installed",
321
+ status: "error",
322
+ detail: "CGC not installed — run: pipx install codegraphcontext",
323
+ });
324
+ return {
325
+ content: [{ type: "text", text: JSON.stringify({ steps }, null, 2) }],
326
+ isError: true,
327
+ };
328
+ }
329
+ steps.push({ step: "cgc-installed", status: "ok", detail: cgc });
330
+ // 2. Check FalkorDB container exists and is running
331
+ try {
332
+ const status = execSync("docker ps --filter name=falkordb --format '{{.Status}}'", {
333
+ encoding: "utf-8",
334
+ timeout: 5000,
335
+ stdio: ["ignore", "pipe", "pipe"],
336
+ }).trim();
337
+ if (status) {
338
+ steps.push({ step: "falkordb-container", status: "ok", detail: status });
339
+ }
340
+ else {
341
+ // Container exists but not running — try to start
342
+ try {
343
+ execSync("docker start falkordb", {
344
+ timeout: 10000,
345
+ stdio: ["ignore", "pipe", "pipe"],
346
+ });
347
+ steps.push({
348
+ step: "falkordb-container",
349
+ status: "fixed",
350
+ detail: "Started existing container",
351
+ });
352
+ }
353
+ catch {
354
+ // Container doesn't exist — create it
355
+ try {
356
+ execSync("docker run -d --name falkordb --restart unless-stopped -v falkordb-global:/var/lib/falkordb/data falkordb/falkordb:latest", { timeout: 30000, stdio: ["ignore", "pipe", "pipe"] });
357
+ steps.push({
358
+ step: "falkordb-container",
359
+ status: "fixed",
360
+ detail: "Created new container",
361
+ });
362
+ }
363
+ catch (e) {
364
+ const err = e;
365
+ steps.push({
366
+ step: "falkordb-container",
367
+ status: "error",
368
+ detail: err.message ?? "Failed to create container",
369
+ });
370
+ }
371
+ }
372
+ }
373
+ }
374
+ catch {
375
+ steps.push({
376
+ step: "falkordb-container",
377
+ status: "error",
378
+ detail: "Docker not available — is OrbStack running?",
379
+ });
380
+ }
381
+ // 3. Check connectivity
382
+ const host = getFalkorHost();
383
+ if (checkFalkorConnection(host)) {
384
+ steps.push({
385
+ step: "falkordb-connection",
386
+ status: "ok",
387
+ detail: `Connected to ${host}:6379`,
388
+ });
389
+ }
390
+ else {
391
+ // Wait a moment if we just started the container
392
+ const justStarted = steps.some((s) => s.step === "falkordb-container" && s.status === "fixed");
393
+ if (justStarted) {
394
+ await new Promise((r) => setTimeout(r, 3000));
395
+ if (checkFalkorConnection(host)) {
396
+ steps.push({
397
+ step: "falkordb-connection",
398
+ status: "ok",
399
+ detail: `Connected to ${host}:6379 (after wait)`,
400
+ });
401
+ }
402
+ else {
403
+ steps.push({
404
+ step: "falkordb-connection",
405
+ status: "error",
406
+ detail: `Cannot connect to ${host}:6379`,
407
+ });
408
+ }
409
+ }
410
+ else {
411
+ steps.push({
412
+ step: "falkordb-connection",
413
+ status: "error",
414
+ detail: `Cannot connect to ${host}:6379`,
415
+ });
416
+ }
417
+ }
418
+ // 4. Check if repo is indexed
419
+ if (steps.every((s) => s.status !== "error")) {
420
+ const listOutput = runCgc("list", projectRoot, { skipConnectionCheck: true });
421
+ if (listOutput.includes(projectRoot) || listOutput.includes(basename(projectRoot))) {
422
+ steps.push({ step: "repo-indexed", status: "ok", detail: "Repository is indexed" });
423
+ }
424
+ else {
425
+ steps.push({
426
+ step: "repo-indexed",
427
+ status: "error",
428
+ detail: "Repository not indexed — call index_project to index",
429
+ });
430
+ }
431
+ }
432
+ const hasErrors = steps.some((s) => s.status === "error");
433
+ const hasFixed = steps.some((s) => s.status === "fixed");
434
+ return {
435
+ content: [
436
+ {
437
+ type: "text",
438
+ text: JSON.stringify({
439
+ healthy: !hasErrors,
440
+ autoRepaired: hasFixed,
441
+ host,
442
+ steps,
443
+ }, null, 2),
444
+ },
445
+ ],
446
+ isError: hasErrors,
447
+ };
448
+ });
271
449
  }
@@ -6,6 +6,10 @@
6
6
  {
7
7
  "name": "falkordb-container",
8
8
  "command": "docker ps --filter name=falkordb --format '{{.Status}}'"
9
+ },
10
+ {
11
+ "name": "falkordb-connection",
12
+ "command": "node -e \"const s=require('net').connect(6379,process.env.FALKORDB_HOST||'falkordb.orb.local');s.setTimeout(3000);s.on('connect',()=>{console.log('connected');s.end();process.exit(0)});s.on('error',e=>{console.error(e.message);process.exit(1)});s.on('timeout',()=>{console.error('timeout');process.exit(1)})\""
9
13
  }
10
14
  ],
11
15
  "env_vars": {
@@ -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.2",
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/catchup.md CHANGED
@@ -64,7 +64,7 @@ Understand what each skill does and when to use it. You should be able to answer
64
64
  **After reviewing, edit the handoff to check off:** `- [x] skills` and `- [x] extensions`
65
65
 
66
66
  ### 7. Check Code Graph
67
- Call `graph_stats` to understand the codebase size and structure. This gives you a sense of what's indexed and queryable. If it fails, call `graph_doctor` to diagnose. If FalkorDB is down or the repo isn't indexed, flag it.
67
+ Call `graph_ensure` to validate the entire code graph stack: FalkorDB container, CGC connection, repo indexing. This tool auto-repairs common issues (starts stopped containers, detects the right host). If it reports errors it couldn't fix, tell the user what's wrong and how to fix it. If the repo isn't indexed, call `index_project`.
68
68
 
69
69
  **After checking, edit the handoff to check off:** `- [x] graph`
70
70
 
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).