delimit-cli 3.13.2 → 3.14.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.
@@ -0,0 +1,201 @@
1
+ """
2
+ Tool Metadata — Tier mappings for all Delimit MCP tools.
3
+
4
+ Each tool is assigned a visibility tier:
5
+ - "public": Always visible to all MCP clients.
6
+ - "ops_pack": Visible when DELIMIT_SHOW_OPS=1 (or SHOW_INTERNAL/SHOW_EXPERIMENTAL).
7
+ - "internal": Visible when DELIMIT_SHOW_INTERNAL=1 (or SHOW_EXPERIMENTAL).
8
+ - "experimental": Visible when DELIMIT_SHOW_EXPERIMENTAL=1.
9
+
10
+ Tier cascade: experimental > internal > ops_pack > public.
11
+
12
+ Reference: Consensus 118/119/120 — Tool Segmentation Architecture.
13
+ """
14
+
15
+ from typing import Dict, Literal
16
+
17
+ Tier = Literal["public", "ops_pack", "internal", "experimental"]
18
+
19
+ # ─────────────────────────────────────────────────────────────────────
20
+ # TOOL_TIERS: canonical tier assignment for every registered tool.
21
+ #
22
+ # ops_pack (~8): workspace automation tools (social, notify, design
23
+ # validation, screenshots, screen recording, docs gen)
24
+ #
25
+ # internal (~18): founder-only tools (content pipeline, design internals,
26
+ # storybook, data ops, social management, scaffolding)
27
+ #
28
+ # experimental: stubs and pass-throughs hidden behind LED-044
29
+ #
30
+ # public: everything else (the default)
31
+ # ─────────────────────────────────────────────────────────────────────
32
+
33
+ TOOL_TIERS: Dict[str, Tier] = {
34
+ # === Govern domain (all public) ===
35
+ "delimit_lint": "public",
36
+ "delimit_diff": "public",
37
+ "delimit_policy": "public",
38
+ "delimit_semver": "public",
39
+ "delimit_explain": "public",
40
+ "delimit_zero_spec": "public",
41
+ "delimit_init": "public",
42
+ "delimit_gov_health": "public",
43
+ "delimit_gov_status": "public",
44
+ "delimit_gov_policy": "public",
45
+ "delimit_gov_evaluate": "public",
46
+ "delimit_gov_new_task": "public",
47
+ "delimit_gov_run": "public",
48
+ "delimit_gov_verify": "public",
49
+ "delimit_ledger": "public",
50
+ "delimit_impact": "public",
51
+
52
+ # === Context domain (all public) ===
53
+ "delimit_memory_search": "public",
54
+ "delimit_memory_store": "public",
55
+ "delimit_memory_recent": "public",
56
+ "delimit_vault_search": "public",
57
+ "delimit_vault_health": "public",
58
+ "delimit_vault_snapshot": "public",
59
+ "delimit_context_init": "public",
60
+ "delimit_context_write": "public",
61
+ "delimit_context_read": "public",
62
+ "delimit_context_list": "public",
63
+ "delimit_context_snapshot": "public",
64
+ "delimit_context_branch": "public",
65
+ "delimit_secret_store": "public",
66
+ "delimit_secret_get": "public",
67
+ "delimit_secret_list": "public",
68
+ "delimit_secret_revoke": "public",
69
+ "delimit_secret_access_log": "public",
70
+
71
+ # === Ship domain (public + experimental) ===
72
+ "delimit_deploy_plan": "public",
73
+ "delimit_deploy_build": "public",
74
+ "delimit_deploy_publish": "public",
75
+ "delimit_deploy_verify": "experimental",
76
+ "delimit_deploy_rollback": "public",
77
+ "delimit_deploy_status": "public",
78
+ "delimit_deploy_site": "public",
79
+ "delimit_deploy_npm": "public",
80
+ "delimit_release_plan": "public",
81
+ "delimit_release_validate": "experimental",
82
+ "delimit_release_status": "public",
83
+ "delimit_release_rollback": "experimental",
84
+ "delimit_release_history": "experimental",
85
+ "delimit_release_sync": "public",
86
+
87
+ # === Observe domain (public + experimental) ===
88
+ "delimit_obs_metrics": "public",
89
+ "delimit_obs_logs": "public",
90
+ "delimit_obs_alerts": "experimental",
91
+ "delimit_obs_status": "public",
92
+ "delimit_cost_analyze": "public",
93
+ "delimit_cost_optimize": "public",
94
+ "delimit_cost_alert": "public",
95
+ "delimit_cost_controls": "public",
96
+
97
+ # === Security domain (all public) ===
98
+ "delimit_security_scan": "public",
99
+ "delimit_security_ingest": "public",
100
+ "delimit_security_deliberate": "public",
101
+ "delimit_security_audit": "public",
102
+ "delimit_evidence_collect": "public",
103
+ "delimit_evidence_verify": "public",
104
+
105
+ # === Intel domain (all public) ===
106
+ "delimit_intel_dataset_register": "public",
107
+ "delimit_intel_dataset_list": "public",
108
+ "delimit_intel_dataset_freeze": "public",
109
+ "delimit_intel_snapshot_ingest": "public",
110
+ "delimit_intel_query": "public",
111
+
112
+ # === Repo domain (experimental stubs) ===
113
+ "delimit_repo_diagnose": "experimental",
114
+ "delimit_repo_analyze": "experimental",
115
+ "delimit_repo_config_validate": "experimental",
116
+ "delimit_repo_config_audit": "experimental",
117
+
118
+ # === Test domain (public + experimental) ===
119
+ "delimit_test_generate": "public",
120
+ "delimit_test_coverage": "experimental",
121
+ "delimit_test_smoke": "public",
122
+
123
+ # === Orchestrate domain (all public) ===
124
+ "delimit_os_plan": "public",
125
+ "delimit_os_status": "public",
126
+ "delimit_os_gates": "public",
127
+ "delimit_ledger_add": "public",
128
+ "delimit_ledger_done": "public",
129
+ "delimit_ledger_list": "public",
130
+ "delimit_ledger_context": "public",
131
+ "delimit_ventures": "public",
132
+ "delimit_models": "public",
133
+ "delimit_deliberate": "public",
134
+ "delimit_sensor_github_issue": "public",
135
+ "delimit_resource_list": "public",
136
+ "delimit_resource_get": "public",
137
+ "delimit_resource_drivers": "public",
138
+ "delimit_tracker_sync": "public",
139
+ "delimit_webhook_manage": "public",
140
+ "delimit_agent_dispatch": "public",
141
+ "delimit_agent_status": "public",
142
+ "delimit_agent_complete": "public",
143
+ "delimit_agent_handoff": "public",
144
+ "delimit_next_task": "public",
145
+ "delimit_task_complete": "public",
146
+ "delimit_loop_status": "public",
147
+ "delimit_loop_config": "public",
148
+ "delimit_daemon_status": "public",
149
+ "delimit_daemon_run": "public",
150
+ "delimit_daemon_classify": "public",
151
+ "delimit_config_export": "public",
152
+ "delimit_config_import": "public",
153
+ "delimit_changelog": "public",
154
+
155
+ # === Utility (all public) ===
156
+ "delimit_version": "public",
157
+ "delimit_help": "public",
158
+ "delimit_diagnose": "public",
159
+ "delimit_activate": "public",
160
+ "delimit_license_status": "public",
161
+ "delimit_scan": "public",
162
+ "delimit_quickstart": "public",
163
+
164
+ # ═══════════════════════════════════════════════════════════════
165
+ # Workspace-ops tier (~8 tools)
166
+ # Visible with DELIMIT_SHOW_OPS=1 or DELIMIT_SHOW_INTERNAL=1
167
+ # ═══════════════════════════════════════════════════════════════
168
+ "delimit_social_post": "ops_pack",
169
+ "delimit_social_generate": "ops_pack",
170
+ "delimit_social_approve": "ops_pack",
171
+ "delimit_notify": "ops_pack",
172
+ "delimit_design_validate_responsive": "ops_pack",
173
+ "delimit_screenshot": "ops_pack",
174
+ "delimit_screen_record": "ops_pack",
175
+ "delimit_docs_generate": "ops_pack",
176
+
177
+ # ═══════════════════════════════════════════════════════════════
178
+ # Internal tier (~18 tools)
179
+ # Visible with DELIMIT_SHOW_INTERNAL=1
180
+ # ═══════════════════════════════════════════════════════════════
181
+ "delimit_content_schedule": "internal",
182
+ "delimit_content_publish": "internal",
183
+ "delimit_content_queue": "internal",
184
+ "delimit_inbox_daemon": "internal",
185
+ "delimit_notify_inbox": "internal",
186
+ "delimit_design_generate_component": "internal",
187
+ "delimit_design_extract_tokens": "internal",
188
+ "delimit_design_component_library": "internal",
189
+ "delimit_design_generate_tailwind": "internal",
190
+ "delimit_story_generate": "internal",
191
+ "delimit_story_build": "internal",
192
+ "delimit_story_visual_test": "internal",
193
+ "delimit_story_accessibility": "internal",
194
+ "delimit_data_backup": "internal",
195
+ "delimit_data_migrate": "internal",
196
+ "delimit_data_validate": "internal",
197
+ "delimit_social_history": "internal",
198
+ "delimit_social_accounts": "internal",
199
+ "delimit_generate_scaffold": "internal",
200
+ "delimit_generate_template": "internal",
201
+ }
@@ -30,6 +30,8 @@ function loadHookConfig() {
30
30
  session_start: true,
31
31
  pre_tool: true,
32
32
  pre_commit: true,
33
+ conditional_hooks: true,
34
+ deploy_audit: true,
33
35
  deliberate_on_commit: false,
34
36
  show_strategy_items: true,
35
37
  };
@@ -125,9 +127,63 @@ function detectAITools() {
125
127
  // Hook installers per tool
126
128
  // ---------------------------------------------------------------------------
127
129
 
130
+ /**
131
+ * Check if a Claude Code hook group array already contains a delimit hook
132
+ * matching the given command substring.
133
+ */
134
+ function findClaudeHookGroup(hookGroups, commandSubstring) {
135
+ if (!Array.isArray(hookGroups)) return null;
136
+ for (const group of hookGroups) {
137
+ // Support both nested format (group.hooks[].command) and flat format (group.command)
138
+ if (group.hooks && Array.isArray(group.hooks)) {
139
+ if (group.hooks.some(h => h.command && h.command.includes(commandSubstring))) {
140
+ return group;
141
+ }
142
+ }
143
+ if (group.command && group.command.includes(commandSubstring)) {
144
+ return group;
145
+ }
146
+ }
147
+ return null;
148
+ }
149
+
150
+ /**
151
+ * Migrate a flat-format hook entry to the nested Claude Code format.
152
+ * Flat: { type, command, matcher, if }
153
+ * Nested: { matcher, if, hooks: [{ type, command }] }
154
+ */
155
+ function migrateToNestedFormat(hookGroup) {
156
+ if (hookGroup.hooks && Array.isArray(hookGroup.hooks)) {
157
+ return hookGroup; // Already nested
158
+ }
159
+ const nested = { matcher: hookGroup.matcher || '' };
160
+ if (hookGroup.if) nested.if = hookGroup.if;
161
+ nested.hooks = [{ type: hookGroup.type || 'command', command: hookGroup.command }];
162
+ return nested;
163
+ }
164
+
128
165
  /**
129
166
  * Install hooks into Claude Code's ~/.claude/settings.json
130
- * Claude Code supports native hooks: SessionStart, PreToolUse, PostToolUse, etc.
167
+ *
168
+ * Claude Code hook format (nested):
169
+ * {
170
+ * "hooks": {
171
+ * "EventName": [
172
+ * {
173
+ * "matcher": "ToolPattern",
174
+ * "if": "condition expression",
175
+ * "hooks": [
176
+ * { "type": "command", "command": "...", "timeout": 30 }
177
+ * ]
178
+ * }
179
+ * ]
180
+ * }
181
+ * }
182
+ *
183
+ * LED-234: Adds conditional hooks that fire only when relevant files change:
184
+ * 1. PostToolUse (Edit|Write) + spec patterns -> delimit lint
185
+ * 2. PreToolUse (Bash) + git commit -> delimit doctor
186
+ * 3. PreToolUse (Bash) + deploy patterns -> delimit security-audit
131
187
  */
132
188
  function installClaudeHooks(tool, hookConfig) {
133
189
  const configPath = tool.configPath;
@@ -148,73 +204,133 @@ function installClaudeHooks(tool, hookConfig) {
148
204
  const npxCmd = 'npx delimit-cli';
149
205
  const changes = [];
150
206
 
151
- // SessionStart hook
207
+ // --- SessionStart hook (no condition) ---
152
208
  if (hookConfig.session_start) {
153
- const sessionHook = {
154
- type: 'command',
155
- command: `${npxCmd} hook session-start`,
156
- };
157
209
  if (!config.hooks.SessionStart) {
158
210
  config.hooks.SessionStart = [];
159
211
  }
160
- // Check if already installed
161
- const existing = config.hooks.SessionStart.find(
162
- h => h.command && h.command.includes('delimit-cli hook session-start')
163
- );
212
+ const existing = findClaudeHookGroup(config.hooks.SessionStart, 'delimit-cli hook session-start');
164
213
  if (!existing) {
165
- config.hooks.SessionStart.push(sessionHook);
214
+ config.hooks.SessionStart.push({
215
+ matcher: '',
216
+ hooks: [{
217
+ type: 'command',
218
+ command: `${npxCmd} hook session-start`,
219
+ }],
220
+ });
166
221
  changes.push('SessionStart');
167
222
  }
168
223
  }
169
224
 
170
- // PreToolUse hook scoped to Edit/Write on OpenAPI/Swagger spec files
225
+ // --- PreToolUse: pre-tool hook scoped to Edit/Write on spec files ---
171
226
  if (hookConfig.pre_tool) {
172
- const preToolHook = {
173
- type: 'command',
174
- command: `${npxCmd} hook pre-tool $TOOL_NAME`,
175
- matcher: 'Edit|Write',
176
- if: "Edit && (path_matches('**/openapi*') || path_matches('**/swagger*') || path_matches('**/*.yaml') || path_matches('**/*.yml'))",
177
- };
178
227
  if (!config.hooks.PreToolUse) {
179
228
  config.hooks.PreToolUse = [];
180
229
  }
181
- const existing = config.hooks.PreToolUse.find(
182
- h => h.command && h.command.includes('delimit-cli hook pre-tool')
183
- );
230
+ const existing = findClaudeHookGroup(config.hooks.PreToolUse, 'delimit-cli hook pre-tool');
184
231
  if (existing) {
185
- // Upgrade existing hook to include conditional `if` field
186
- if (!existing.if) {
187
- existing.matcher = preToolHook.matcher;
188
- existing.command = preToolHook.command;
189
- existing.if = preToolHook.if;
232
+ // Upgrade flat-format hook to nested + add if condition if missing
233
+ const migrated = migrateToNestedFormat(existing);
234
+ if (!migrated.if) {
235
+ const idx = config.hooks.PreToolUse.indexOf(existing);
236
+ migrated.matcher = 'Edit|Write';
237
+ migrated.if = "Edit && (path_matches('**/openapi*') || path_matches('**/swagger*') || path_matches('**/*.yaml') || path_matches('**/*.yml'))";
238
+ migrated.hooks = [{ type: 'command', command: `${npxCmd} hook pre-tool $TOOL_NAME` }];
239
+ config.hooks.PreToolUse[idx] = migrated;
190
240
  changes.push('PreToolUse (upgraded)');
191
241
  }
192
242
  } else {
193
- config.hooks.PreToolUse.push(preToolHook);
243
+ config.hooks.PreToolUse.push({
244
+ matcher: 'Edit|Write',
245
+ if: "Edit && (path_matches('**/openapi*') || path_matches('**/swagger*') || path_matches('**/*.yaml') || path_matches('**/*.yml'))",
246
+ hooks: [{
247
+ type: 'command',
248
+ command: `${npxCmd} hook pre-tool $TOOL_NAME`,
249
+ }],
250
+ });
194
251
  changes.push('PreToolUse');
195
252
  }
196
253
  }
197
254
 
198
- // PreToolUse hook — pre-commit governance on git commit/push
255
+ // --- PreToolUse: pre-commit governance on git commit/push ---
199
256
  if (hookConfig.pre_commit) {
200
- const preCommitHook = {
201
- type: 'command',
202
- command: `${npxCmd} hook pre-commit`,
203
- matcher: 'Bash',
204
- if: "Bash && (input_contains('git commit') || input_contains('git push'))",
205
- };
206
257
  if (!config.hooks.PreToolUse) {
207
258
  config.hooks.PreToolUse = [];
208
259
  }
209
- const existing = config.hooks.PreToolUse.find(
210
- h => h.command && h.command.includes('delimit-cli hook pre-commit')
211
- );
260
+ const existing = findClaudeHookGroup(config.hooks.PreToolUse, 'delimit-cli hook pre-commit');
212
261
  if (!existing) {
213
- config.hooks.PreToolUse.push(preCommitHook);
262
+ config.hooks.PreToolUse.push({
263
+ matcher: 'Bash',
264
+ if: "Bash && (input_contains('git commit') || input_contains('git push'))",
265
+ hooks: [{
266
+ type: 'command',
267
+ command: `${npxCmd} hook pre-commit`,
268
+ }],
269
+ });
214
270
  changes.push('PreCommit');
215
271
  }
216
272
  }
217
273
 
274
+ // --- LED-234: Conditional hooks (opt-in via conditional_hooks config) ---
275
+ if (hookConfig.conditional_hooks !== false) {
276
+
277
+ // 1. PostToolUse: auto-lint after editing OpenAPI spec files
278
+ if (!config.hooks.PostToolUse) {
279
+ config.hooks.PostToolUse = [];
280
+ }
281
+ const specLintCmd = 'delimit-cli lint';
282
+ const existingSpecLint = findClaudeHookGroup(config.hooks.PostToolUse, specLintCmd);
283
+ if (!existingSpecLint) {
284
+ config.hooks.PostToolUse.push({
285
+ matcher: 'Edit|Write',
286
+ if: "filePath matches '**/*openapi*.yaml' or filePath matches '**/*openapi*.yml' or filePath matches '**/*openapi*.json' or filePath matches '**/*swagger*.yaml' or filePath matches '**/*swagger*.yml' or filePath matches '**/*swagger*.json' or filePath matches '**/api/*.yaml' or filePath matches '**/api/*.yml' or filePath matches '**/specs/**'",
287
+ hooks: [{
288
+ type: 'command',
289
+ command: `${npxCmd} lint "$DELIMIT_FILE_PATH"`,
290
+ timeout: 30,
291
+ }],
292
+ });
293
+ changes.push('PostToolUse:spec-lint');
294
+ }
295
+
296
+ // 2. PreToolUse: repo diagnose before git commit (uses doctor command)
297
+ if (!config.hooks.PreToolUse) {
298
+ config.hooks.PreToolUse = [];
299
+ }
300
+ const doctorCmd = 'delimit-cli doctor';
301
+ const existingDoctor = findClaudeHookGroup(config.hooks.PreToolUse, doctorCmd);
302
+ if (!existingDoctor) {
303
+ config.hooks.PreToolUse.push({
304
+ matcher: 'Bash',
305
+ if: "command matches 'git commit'",
306
+ hooks: [{
307
+ type: 'command',
308
+ command: `${npxCmd} doctor`,
309
+ timeout: 15,
310
+ }],
311
+ });
312
+ changes.push('PreToolUse:doctor');
313
+ }
314
+
315
+ // 3. PreToolUse: security audit before deploy/publish/release commands
316
+ if (hookConfig.deploy_audit !== false) {
317
+ const securityCmd = 'delimit-cli security-audit';
318
+ const existingSecurity = findClaudeHookGroup(config.hooks.PreToolUse, securityCmd);
319
+ if (!existingSecurity) {
320
+ config.hooks.PreToolUse.push({
321
+ matcher: 'Bash',
322
+ if: "command matches 'npm publish' or command matches 'npx deploy' or command matches 'deploy' or command matches 'release'",
323
+ hooks: [{
324
+ type: 'command',
325
+ command: `${npxCmd} security-audit`,
326
+ timeout: 30,
327
+ }],
328
+ });
329
+ changes.push('PreToolUse:deploy-audit');
330
+ }
331
+ }
332
+ }
333
+
218
334
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
219
335
  return changes;
220
336
  }
@@ -235,10 +351,12 @@ function installCodexHooks(tool, hookConfig) {
235
351
  const delimitBlock = `<!-- delimit:hooks-start -->
236
352
  ## Delimit Governance
237
353
 
238
- On every session start, run this command to check governance status:
354
+ On every session start:
355
+ 1. Run this command to check governance status:
239
356
  \`\`\`
240
357
  npx delimit-cli hook session-start
241
358
  \`\`\`
359
+ 2. Call \`delimit_inbox_daemon(action="start")\` to start email polling for approvals.
242
360
 
243
361
  Before committing changes, run:
244
362
  \`\`\`
@@ -311,7 +429,8 @@ function installGeminiHooks(tool, hookConfig) {
311
429
 
312
430
  const govInstructions = [
313
431
  'Delimit governance is active.',
314
- 'On session start, run: npx delimit-cli hook session-start',
432
+ 'On session start: (1) run: npx delimit-cli hook session-start,',
433
+ '(2) call delimit_inbox_daemon(action="start") to start email polling.',
315
434
  'Before committing, run: npx delimit-cli hook pre-commit',
316
435
  'After editing API specs, run: npx delimit-cli hook pre-tool Edit',
317
436
  'Call delimit_ledger_context and delimit_gov_health via MCP when available.',
@@ -331,6 +450,7 @@ function installGeminiHooks(tool, hookConfig) {
331
450
 
332
451
  On every session start:
333
452
  1. Run \`npx delimit-cli hook session-start\` to check open tasks and governance status
453
+ 2. Call \`delimit_inbox_daemon(action="start")\` to start email polling for approvals
334
454
 
335
455
  After editing code:
336
456
  - After editing API specs: run \`npx delimit-cli hook pre-tool Edit\`
@@ -392,9 +512,14 @@ function removeClaudeHooks() {
392
512
  for (const event of ['SessionStart', 'PreToolUse', 'PostToolUse']) {
393
513
  if (Array.isArray(config.hooks[event])) {
394
514
  const before = config.hooks[event].length;
395
- config.hooks[event] = config.hooks[event].filter(
396
- h => !(h.command && h.command.includes('delimit-cli'))
397
- );
515
+ config.hooks[event] = config.hooks[event].filter(h => {
516
+ // Nested format: check hooks[].command
517
+ if (h.hooks && Array.isArray(h.hooks)) {
518
+ return !h.hooks.some(inner => inner.command && inner.command.includes('delimit-cli'));
519
+ }
520
+ // Flat format: check h.command directly
521
+ return !(h.command && h.command.includes('delimit-cli'));
522
+ });
398
523
  if (config.hooks[event].length === 0) {
399
524
  delete config.hooks[event];
400
525
  }
@@ -671,6 +796,9 @@ async function hookSessionStart() {
671
796
  }
672
797
  } catch { /* not in git repo */ }
673
798
 
799
+ // Remind AI to start inbox daemon
800
+ lines.push('[Delimit] Action required: call delimit_inbox_daemon(action="start") to enable email-based approvals');
801
+
674
802
  lines.push('');
675
803
  process.stdout.write(lines.join('\n') + '\n');
676
804
  }
@@ -849,4 +977,6 @@ module.exports = {
849
977
  hookPreCommit,
850
978
  countPendingStrategyItems,
851
979
  getTopStrategyItem,
980
+ findClaudeHookGroup,
981
+ migrateToNestedFormat,
852
982
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "delimit-cli",
3
3
  "mcpName": "io.github.delimit-ai/delimit-mcp-server",
4
- "version": "3.13.2",
4
+ "version": "3.14.0",
5
5
  "description": "Unify Claude Code, Codex, Cursor, and Gemini CLI with persistent context, governance, and multi-model debate.",
6
6
  "main": "index.js",
7
7
  "files": [