@descix/cli 1.0.17 → 1.0.19

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.
Files changed (38) hide show
  1. package/agent-assets/guides/README.md +2 -1
  2. package/agent-assets/guides/dynamic-site-pattern.md +6 -10
  3. package/agent-assets/guides/knowledge-only-pattern.md +12 -11
  4. package/agent-assets/guides/microservice-pattern.md +13 -16
  5. package/agent-assets/guides/static-site-pattern.md +6 -7
  6. package/agent-assets/instructions/app-asset-validation.md +61 -59
  7. package/agent-assets/instructions/onboarding-flow.md +57 -74
  8. package/agent-assets/instructions/standard-folder-structure.md +39 -36
  9. package/agent-assets/reference/app-creation.md +27 -33
  10. package/agent-assets/reference/architecture.md +85 -148
  11. package/agent-assets/reference/local-dev.md +5 -83
  12. package/agent-assets/reference/mcp-integration.md +59 -49
  13. package/agent-assets/reference/workspace-config.md +9 -10
  14. package/bin/descix.js +20 -151
  15. package/lib/command-visibility.js +1 -1
  16. package/lib/commands/airdrop.js +6 -1
  17. package/lib/commands/init.js +2 -2
  18. package/lib/commands/kb-sync-surface.js +10 -0
  19. package/lib/commands/kb.js +1 -1
  20. package/lib/commands/microservice-auth-model.js +19 -0
  21. package/lib/commands/retired-verbs.js +106 -0
  22. package/lib/help-guard.js +85 -0
  23. package/lib/service-api-client.js +7 -6
  24. package/lib/verb-invokes.generated.json +0 -5
  25. package/lib/workspace-config.js +1 -1
  26. package/package.json +3 -3
  27. package/templates/agent-claude.md +6 -4
  28. package/templates/agent-cline.md +2 -2
  29. package/templates/agent-copilot.md +2 -2
  30. package/templates/agent-cursor.md +2 -2
  31. package/templates/scaffolds/microservice/SERVICE_README_sdk.md +6 -5
  32. package/templates/scaffolds/microservice/app.js +7 -3
  33. package/templates/scaffolds/microservice/defaults-config.json +2 -1
  34. package/templates/scaffolds/microservice/dev-overrides.example.json +2 -1
  35. package/templates/scaffolds/microservice/manifest.json +2 -1
  36. package/templates/scaffolds/microservice/package.json +2 -2
  37. package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +3 -2
  38. package/lib/commands/retired-kb-sync.js +0 -112
@@ -10,7 +10,7 @@ This document covers how AI agents interact with the DeSciX platform via MCP (Mo
10
10
 
11
11
  ## 1. Architecture Overview
12
12
 
13
- DeSciX operates as a **Federated MCP Service Mesh**. The `DeSciX_Cloud` backend acts as a broker that aggregates tools from all registered microservices into a unified catalog.
13
+ DeSciX operates as a **Federated MCP Service Mesh**. The DeSciX backend acts as a broker: it registers tools from all registered microservices alongside its own native commands, dispatches calls to whichever service owns the command, and advertises a small handshake floor of that set at `tools/list` (see §2.2).
14
14
 
15
15
  **Key Innovation:** AI agents discover and invoke capabilities across the entire mesh without knowing where individual services are hosted.
16
16
 
@@ -24,7 +24,7 @@ AI Agent (Cursor/MCP Client)
24
24
  execute_remote_command
25
25
  │
26
26
  ▼
27
- DeSciX_Cloud (Federated Broker)
27
+ DeSciX Backend (Federated Broker)
28
28
  │
29
29
  ├──► Service 1 (Powch)
30
30
  ├──► Service 2 (SmartModel)
@@ -42,18 +42,33 @@ MCP provides the communication protocol between AI agents and the DeSciX platfor
42
42
  **Key Tools:**
43
43
  - `tell_me_how` - Semantic tool discovery
44
44
  - `execute_remote_command` - Execute any backend command
45
- - `descix_init` - Initialize local workspace
46
- - `chat_with_kb` - Query knowledge bases
47
- - `search_knowledge_base` - Search specific KBs
45
+ - `descix_doctor` - CLI-local startup diagnostic (auth, workspace, tools, warnings)
46
+ - `ask_question_to_app` - RAG chat with an app's knowledge base (synthesized answer, cited)
47
+ - `query_knowledge_base` - Raw vector search, returns chunks
48
48
 
49
- ### 2.2 Cursor/AI Agent Integration
49
+ ### 2.2 The Two-Hop: Reaching Commands That Are Not In Your Tool List
50
50
 
51
- When an AI agent (e.g., Cursor) is configured with the DeSciX MCP server:
51
+ **Your MCP tool list is a small floor, not the platform's capability surface.** The platform
52
+ registers many more commands than it advertises at the handshake, so that a session pays ~1k
53
+ tokens of catalog instead of ~8.7k. Everything outside the advertised floor stays registered and
54
+ callable — you reach it with two hops:
52
55
 
53
- 1. Agent receives user question
54
- 2. Agent uses `tell_me_how` to find relevant tools
55
- 3. Agent calls `execute_remote_command` with discovered tool
56
- 4. Result returned to user
56
+ 1. `tell_me_how({ question })` — names the command and its parameters
57
+ 2. `execute_remote_command({ command, params })` — invokes it
58
+
59
+ **Rule for an agent: if a capability is not a tool in your list, do not conclude the platform
60
+ lacks it, and do not guess a command name — ask `tell_me_how`, then `execute_remote_command`
61
+ what it names.** This is the standing integration pattern for every caller, on every transport.
62
+
63
+ The advertised floor is a **floor, not a ceiling**: `tools/list` serves it UNION the calling
64
+ identity's granted command names, so a caller may see more, never less. The floor's membership
65
+ and its contract are owned by `describeDiscoveryCoreFence()` /
66
+ `DISCOVERY_CORE_TOOL_NAMES` in `@descix/platform-api`
67
+ (`src/mcp-tools/nativeTools.js`) — read your live `tools/list` rather than a copy of it.
68
+
69
+ The CLI's stdio MCP server additionally concatenates its CLI-local diagnostics (`descix_doctor`,
70
+ `platform_health`) onto whatever the backend advertises; those are local CLI operations, never
71
+ `/apifront` commands, so `execute_remote_command` does not reach them.
57
72
 
58
73
  ### 2.3 The `_descix` Context Object
59
74
 
@@ -91,13 +106,17 @@ Once signed in, `tell_me_how` is the **primary entry point** for discovering pla
91
106
 
92
107
  **Rule:** Sign in, then ask `tell_me_how` first, then execute the recommended command.
93
108
 
94
- ### 3.2 Three Scopes
109
+ ### 3.2 Scopes
110
+
111
+ The validated `tell_me_how` scope enum has five values:
95
112
 
96
113
  | Scope | Description | Use Case |
97
114
  |-------|-------------|----------|
98
- | `entitlements` (default) | Only tools user has access to | Production usage |
115
+ | `entitlements` (default) | Only tools the user has access to | Production usage |
99
116
  | `project` | Tools filtered by workspace.json | Working within a project |
100
117
  | `discovery` | All available tools | Exploring capabilities |
118
+ | `bootstrap` | Deterministic first-call on-ramp: platform summary, caller context, credit balance, essential tool schemas (no vector search; `question` is optional for this scope only) | First contact / first call in a session |
119
+ | `artifact` | Deterministic build/reproduce provenance for a published app (npm package + spec, artifact/notebook URLs, runnable `npx` commands), no vector search | "How do I build on / reproduce this app?" |
101
120
 
102
121
  ### 3.3 CLI Usage
103
122
 
@@ -151,21 +170,23 @@ After `descix quickstart` completes:
151
170
 
152
171
  ### 4.1 Federated MCP Broker
153
172
 
154
- `DeSciX_Cloud` aggregates tools from all registered microservices:
173
+ The DeSciX backend registers tools from all registered microservices and makes them callable and
174
+ semantically discoverable — the handshake advertises only the floor (§2.2):
155
175
 
156
176
  ```
157
177
  Microservice A
158
178
  └── manifest.json (tools: [tool1, tool2])
159
179
  │
160
180
  ▼
161
- Registers with Core
162
- │
181
+ Registers with Core ──► callable via execute_remote_command
182
+ │ + vectorized for tell_me_how
163
183
  ▼
164
- DeSciX_Cloud (Broker)
165
- └── Aggregated tools/list
184
+ DeSciX Backend (Broker)
185
+ └── tools/list ──► handshake floor + this caller's grants
166
186
  │
167
187
  ▼
168
- AI Agent sees: [tool1, tool2, core_tools...]
188
+ AI Agent's tool list: the floor. tool1/tool2 are reached
189
+ with tell_me_how -> execute_remote_command.
169
190
  ```
170
191
 
171
192
  ### 4.2 SERVICE_README Vectorization
@@ -199,14 +220,11 @@ execute_remote_command({
199
220
 
200
221
  ### 4.3 Service Registration
201
222
 
202
- Services self-register with the broker:
223
+ Services self-register with the broker in one step — registration also vectorizes the README you
224
+ pass, so there is no separate developer-facing vectorize step:
203
225
 
204
226
  ```bash
205
- # Register service manifest
206
- descix microservice register -m ./manifest.json
207
-
208
- # Vectorize SERVICE_README for discovery
209
- descix microservice vectorize -r SERVICE_README.md
227
+ descix microservice register -r SERVICE_README.md
210
228
  ```
211
229
 
212
230
  ### 4.4 The execute_remote_command Pattern
@@ -309,11 +327,13 @@ When `--scope project` is used:
309
327
 
310
328
  ### 6.1 KB Operations via tell_me_how
311
329
 
312
- The new SDK V2 KB commands are discoverable:
330
+ `tell_me_how` can be asked about KB sync in plain language, but the actionable answer for a
331
+ developer is always the CLI verb, not the raw backend command names (`kb_sync_chunks` etc. are
332
+ internal to the backend, not something the CLI calls directly by name):
313
333
 
314
334
  ```bash
315
335
  descix tell-me-how "How do I sync my knowledge base?"
316
- # Returns: kb_sync_chunks, kb_get_chunk_ids, kb_delete_chunks
336
+ # The developer-facing command either way: descix kb corpus sync
317
337
  ```
318
338
 
319
339
  ### 6.2 workspace.json from Hydration
@@ -324,7 +344,7 @@ descix tell-me-how "How do I sync my knowledge base?"
324
344
  {
325
345
  "env": { "apiUrl": "https://dev.descix.net", "products": [ /* one entry per app */ ] },
326
346
  "driveConfig": {
327
- "base_folder_id": "..." // For tell_me_how project scope
347
+ "base_folder_id": "..." // Only used by `descix drive pull/push` — unrelated to tell_me_how scope
328
348
  }
329
349
  }
330
350
  ```
@@ -334,9 +354,9 @@ descix tell-me-how "How do I sync my knowledge base?"
334
354
  Custom services integrate with the same pattern:
335
355
 
336
356
  1. Create `SERVICE_README_{name}.md`
337
- 2. Register with `descix microservice register`
338
- 3. Vectorize with `descix microservice vectorize`
339
- 4. Tools become discoverable via `tell_me_how`
357
+ 2. Register with `descix microservice register -r SERVICE_README_{name}.md` — this both publishes
358
+ the manifest AND vectorizes the README in one step
359
+ 3. Tools become discoverable via `tell_me_how`
340
360
 
341
361
  ---
342
362
 
@@ -355,7 +375,8 @@ Custom services integrate with the same pattern:
355
375
  1. **Document all commands in SERVICE_README** - Required for discovery
356
376
  2. **Use clear descriptions** - "Use when:" is crucial for semantic matching
357
377
  3. **Include examples** - Helps AI agents understand usage
358
- 4. **Register after changes** - Re-vectorize when README updates
378
+ 4. **Re-register after README changes** - `descix microservice register -r <file>` re-vectorizes
379
+ automatically; there is no separate vectorize step
359
380
 
360
381
  ### 7.3 Common Patterns
361
382
 
@@ -407,22 +428,11 @@ cd /path/to/workspace
407
428
 
408
429
  ### 8.3 "Command not found"
409
430
 
410
- **Cause:** Service not registered or command deprecated.
431
+ **Cause:** Service not registered, or the README wasn't vectorized.
411
432
 
412
433
  **Solution:**
413
- 1. Verify service is registered: `descix microservice list`
414
- 2. Check if command was deprecated in V2
415
- 3. Use `tell_me_how --scope discovery` to find alternatives
416
-
417
- ---
418
-
419
- ## 9. File References
420
-
421
- | Component | Path |
422
- |-----------|------|
423
- | MCP Tools | `DeSciX_Core/descix-cli/bin/mcp-server.js` |
424
- | tell_me_how Implementation | `DeSciX_Cloud/microservice/services/commandHandlers/ragCommands.js` |
425
- | execute_remote_command | `DeSciX_Cloud/microservice/services/apiFront.js` |
426
- | SERVICE_README Template | `DeSciX_Core/descix-cli/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md` |
427
- | Workspace Config | `DeSciX_Core/descix-cli/lib/workspace-config.js` |
428
- | Service Registration | `DeSciX_Core/descix-cli/bin/descix.js` |
434
+ 1. Re-run `descix microservice register -r SERVICE_README_<name>.md` — it re-publishes the
435
+ manifest and re-vectorizes the README (`descix microservice list` exists to enumerate
436
+ registered services, but it is an admin-only command, not something a developer runs)
437
+ 2. Check `descix microservice health <name>` to confirm the service itself is reachable
438
+ 3. Use `descix tell-me-how --scope discovery "..."` to find alternatives
@@ -160,13 +160,12 @@ descix site status -c egpt -a egpt-mydocs # site status/list need both when
160
160
 
161
161
  ---
162
162
 
163
- ## 7. File References
164
-
165
- | Component | Path | Description |
166
- |-----------|------|-------------|
167
- | WorkspaceConfig | `DeSciX_Core/descix-cli/lib/workspace-config.js` | the loader, the v2.1 writer, `registerApp`, `detectContext` |
168
- | Origin resolution | `DeSciX_Core/descix-cli/lib/origin.js` | flag → `DESCIX_API_URL` → `env.apiUrl` → default |
169
- | GlobalConfig | `DeSciX_Core/descix-cli/lib/global-config.js` | user-level settings (`~/.descixrc`) |
170
- | Config commands | `DeSciX_Core/descix-cli/lib/commands/config.js` | `config init/set-env/set-*` |
171
- | Wallet file | `DeSciX_Core/descix-cli/lib/wallet-file.js` | `.descix/wallet.json` location |
172
- | Corpus sync | `DeSciX_Core/descix-cli/lib/commands/corpus.js` | manifests, sync-state |
163
+ ## 7. Where each concept lives (conceptual, not a file map)
164
+
165
+ | Concept | Owner |
166
+ |---|---|
167
+ | Workspace loading, the v2.1 writer, app registration, context detection | `WorkspaceConfig`, internal to `@descix/cli` |
168
+ | Origin resolution precedence | flag → `DESCIX_API_URL` → `env.apiUrl` → default (`descix config` commands) |
169
+ | User-level settings (`~/.descixrc`) | `GlobalConfig`, internal to `@descix/cli` |
170
+ | `.descix/wallet.json` | written by `descix login` |
171
+ | Corpus manifests, sync-state | `descix kb corpus` commands, internal to `@descix/cli` |
package/bin/descix.js CHANGED
@@ -11,7 +11,7 @@ import { Command } from 'commander';
11
11
  import chalk from 'chalk';
12
12
  import { DeSciXApiClient } from '../lib/api-client.js';
13
13
  import { requireAuth } from '../lib/auth-guard.js';
14
- import { WorkspaceConfig, unmappedAppMessage, resolveWorkspacePath, TOP_LEVEL_API_URL_REMEDY } from '../lib/workspace-config.js';
14
+ import { WorkspaceConfig, unmappedAppMessage, resolveWorkspacePath } from '../lib/workspace-config.js';
15
15
  import { CLI_VERSION } from '../lib/cli-version.js';
16
16
  import { recordInvocationOrigin } from '../lib/origin.js';
17
17
  // Chat session pointer + the ONE rule for when a dead pointer may be self-healed.
@@ -28,13 +28,16 @@ import * as configCommands from '../lib/commands/config.js';
28
28
  import * as buyCommands from '../lib/commands/buy.js';
29
29
  import * as creditsCommands from '../lib/commands/credits.js';
30
30
  import * as airdropCommands from '../lib/commands/airdrop.js';
31
+ import { MICROSERVICE_AUTH_MODEL } from '../lib/commands/microservice-auth-model.js';
31
32
  import { runInit } from '../lib/commands/init.js';
32
33
  // "May I prompt?" has ONE OWNER. No command in this file derives it.
33
34
  import { createPromptSession } from '../lib/interactive.js';
34
- import { registerAllRetiredKbSync, CANONICAL_KB_SYNC } from '../lib/commands/retired-kb-sync.js';
35
+ import { CANONICAL_KB_SYNC } from '../lib/commands/kb-sync-surface.js';
36
+ import { registerAllRetiredVerbs } from '../lib/commands/retired-verbs.js';
35
37
  import { runStatus } from '../lib/commands/status.js';
36
38
  import { refreshCommunityIdentity, printIdentityReceipt } from '../lib/commands/communityIdentity.js';
37
39
  import { runAppInit } from '../lib/commands/appInit.js';
40
+ import { assertKnownCommandForHelp } from '../lib/help-guard.js';
38
41
  import { runDoctor } from '../lib/commands/doctor.js';
39
42
  import { runHealth } from '../lib/commands/health.js';
40
43
  import * as kbCommands from '../lib/commands/kb.js';
@@ -454,9 +457,9 @@ airdropCommand
454
457
 
455
458
  // ============ Sync Commands: REMOVED ============
456
459
  // `sync assets`, `sync site` and `sync kb` are all gone, so the `sync` GROUP is gone with
457
- // them: a registered verb with no working children is dead weight. Every retired surface is
458
- // registered in ONE place below (search registerAllRetiredKbSync), driven off the owner list
459
- // in lib/commands/retired-kb-sync.js -- this file names no retired verb itself.
460
+ // them: a registered verb with no working children is dead weight. Every retired verb is
461
+ // registered in ONE place below (search registerAllRetiredVerbs), driven off the owner table
462
+ // in lib/commands/retired-verbs.js -- this file names no retired verb itself.
460
463
 
461
464
  // ============ Community/App Commands ============
462
465
 
@@ -1950,12 +1953,8 @@ kbCommand
1950
1953
  });
1951
1954
 
1952
1955
  // `kb chunk` and `kb sync` (low-level Git-mode steps) are REMOVED. Their implementations
1953
- // runKbChunk/runKbSync are deleted from lib/commands/kb.js. Both names exit non-zero naming
1954
- // `descix kb corpus sync`.
1955
- // EVERY retired kb-sync surface is registered here, by ITERATING the owner list -- no verb
1956
- // name is typed in this file. Adding a surface to RETIRED_KB_SYNC_SURFACES registers it;
1957
- // nothing can be registered without being in that list.
1958
- registerAllRetiredKbSync({ program, kb: kbCommand }, chalk.red);
1956
+ // runKbChunk/runKbSync are deleted from lib/commands/kb.js. Both names are rows in
1957
+ // lib/commands/retired-verbs.js, registered with the rest of that table below.
1959
1958
 
1960
1959
 
1961
1960
  // app records — APP DATA PLANE structured record store (CEO-D-2026-06-02-APP-DATA-PLANE)
@@ -2928,6 +2927,7 @@ microserviceCommand
2928
2927
  microserviceCommand
2929
2928
  .command('register')
2930
2929
  .description('Register microservice with gateway (-r <local SERVICE_README>; git-mode only — no Drive)')
2930
+ .addHelpText('after', `\nAuthentication:\n ${MICROSERVICE_AUTH_MODEL}\n`)
2931
2931
  .option('-m, --manifest <path>', 'Path to manifest.json', './manifest.json')
2932
2932
  .option('-r, --readme <path>', 'Path to local SERVICE_README file')
2933
2933
  .option('-c, --community <id>', 'Community ID (auto-detects from context)')
@@ -3288,128 +3288,6 @@ microserviceCommand
3288
3288
  }
3289
3289
  });
3290
3290
 
3291
- // microservice register-delegate - Register delegate key
3292
- microserviceCommand
3293
- .command('register-delegate')
3294
- .description('Provision the service delegate key (SERVICE_KEY) that authenticates mesh/loopback calls — run this if your service gets HTTP 401 calling /apifront or another service')
3295
- .option('-c, --community <id>', 'Community ID (auto-detects from context)')
3296
- .option('-a, --app <id>', 'App ID (auto-detects from context)')
3297
- .option('-s, --slot <id>', 'Service slot ID (uses first available if not provided)')
3298
- .action(async (options) => {
3299
- try {
3300
- const apiClient = new DeSciXApiClient();
3301
- await requireAuth(apiClient);
3302
-
3303
- // Auto-detect context
3304
- const workspaceConfig = await WorkspaceConfig.load();
3305
- const ctx = workspaceConfig.resolveContextWithOptions(options);
3306
-
3307
- let communityId = ctx.communityId;
3308
- let appId = ctx.appId;
3309
-
3310
- // Try manifest.json if not detected
3311
- if (!communityId || !appId) {
3312
- try {
3313
- const manifestContent = await fs.readFile('./manifest.json', 'utf-8');
3314
- const manifest = JSON.parse(manifestContent);
3315
- if (!communityId) communityId = manifest.service?.community_id;
3316
- if (!appId) appId = manifest.service?.app_id;
3317
- } catch {
3318
- // Not found
3319
- }
3320
- }
3321
-
3322
- if (!communityId || !appId) {
3323
- console.error(chalk.red('\n❌ Community and App ID required.'));
3324
- console.log(chalk.gray(' Either provide -c and -a flags, cd into an app directory, or have manifest.json present.\n'));
3325
- process.exit(1);
3326
- }
3327
-
3328
- console.log(chalk.cyan('\n🔑 Registering Service Delegate Key\n'));
3329
-
3330
- // Fetch entitlements
3331
- const entitlementsResponse = await apiClient.invoke('fetch_my_purchases', {});
3332
- const entitlements = entitlementsResponse.message || {};
3333
- const allSlots = entitlements.service_slots || [];
3334
-
3335
- // Service slots come ONLY from subscriptions right now. NFT-based slots are FUTURE
3336
- // functionality (app/NFT association is not wired yet) — never select them here.
3337
- // CEO-D-2026-06-01-MESH-AUTH-DRIVE-REMOVAL (Fix C).
3338
- const serviceSlots = allSlots.filter(slot => slot.type === 'subscription');
3339
-
3340
- if (serviceSlots.length === 0) {
3341
- console.error(chalk.red('❌ No subscription service slot available.'));
3342
- console.log(chalk.white(' Service slots are provided by a subscription. A subscription is required to'));
3343
- console.log(chalk.white(' provision a delegate. (NFT-based slots are future functionality and are'));
3344
- console.log(chalk.white(' not selectable yet.)'));
3345
- process.exit(1);
3346
- }
3347
-
3348
- // Select slot (subscription slots only)
3349
- let selectedSlot = serviceSlots[0];
3350
- if (options.slot) {
3351
- selectedSlot = serviceSlots.find(s => (s.id || s.nft_id) === options.slot);
3352
- if (!selectedSlot) {
3353
- console.error(chalk.red(`❌ Subscription slot ${options.slot} not found in your entitlements.`));
3354
- console.log(chalk.gray(' Only subscription slots are selectable. Run without -s to use the first available.'));
3355
- process.exit(1);
3356
- }
3357
- }
3358
-
3359
- console.log(chalk.gray(` Selected slot: ${selectedSlot.name} (${selectedSlot.type})\n`));
3360
-
3361
- // Generate key pair
3362
- const crypto = await import('crypto');
3363
- const { publicKey, privateKey } = crypto.generateKeyPairSync('ec', {
3364
- namedCurve: 'secp256k1',
3365
- publicKeyEncoding: { type: 'spki', format: 'pem' },
3366
- privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
3367
- });
3368
-
3369
- console.log(chalk.gray(' Generating key pair...'));
3370
-
3371
- // Register with Virtual Registry
3372
- const registerResponse = await apiClient.invoke('register_delegate', {
3373
- slot_id: selectedSlot.nft_id || selectedSlot.id,
3374
- slot_type: selectedSlot.type,
3375
- public_key: publicKey,
3376
- app_id: appId,
3377
- community_id: communityId
3378
- });
3379
-
3380
- console.log(chalk.green(' ✓ Delegate registered with Core\n'));
3381
-
3382
- // Save to dev-overrides.json
3383
- const overridesPath = path.resolve(process.cwd(), 'dev-overrides.json');
3384
- let overrides = {};
3385
-
3386
- try {
3387
- const content = await fs.readFile(overridesPath, 'utf-8');
3388
- overrides = JSON.parse(content);
3389
- } catch (e) {
3390
- // File doesn't exist, start fresh
3391
- }
3392
-
3393
- overrides.SERVICE_KEY = {
3394
- privateKey,
3395
- publicKey,
3396
- slotId: selectedSlot.nft_id || selectedSlot.id,
3397
- slotType: selectedSlot.type,
3398
- appId: appId,
3399
- communityId: communityId,
3400
- createdAt: new Date().toISOString()
3401
- };
3402
-
3403
- await fs.writeFile(overridesPath, JSON.stringify(overrides, null, 2));
3404
- console.log(chalk.green(`✅ Key saved to dev-overrides.json\n`));
3405
- console.log(chalk.gray(` Path: ${overridesPath}\n`));
3406
-
3407
- } catch (error) {
3408
- console.error(chalk.red(`\n❌ Error: ${error.message}\n`));
3409
- process.exit(1);
3410
- }
3411
- });
3412
-
3413
3291
  // microservice restart - Kill + relaunch a local microservice (DEV only)
3414
3292
  microserviceCommand
3415
3293
  .command('restart <name>')
@@ -3862,24 +3740,6 @@ configCommand
3862
3740
  }
3863
3741
  });
3864
3742
 
3865
- // `config set-url` is RETIRED, and it is retired LOUDLY rather than deleted outright: a deleted
3866
- // subcommand only earns commander's "unknown command", which names no replacement. It used to
3867
- // assign a top-level `apiUrl` that save() never serialized, then print a success banner over a
3868
- // value that landed nowhere — the file was re-stamped and the origin never moved. Hidden from
3869
- // --help; any invocation exits non-zero naming the three surfaces that actually set the origin.
3870
- configCommand
3871
- .command('set-url', { hidden: true })
3872
- .description('retired — see error text')
3873
- .argument('[url]')
3874
- .allowUnknownOption()
3875
- .action(() => {
3876
- fail(new Error(
3877
- '"descix config set-url" is retired: it never wrote the origin it reported (a retired top-level\n' +
3878
- 'key that nothing reads). Set the API origin where it is read, env.apiUrl, with one of:\n' +
3879
- ` ${TOP_LEVEL_API_URL_REMEDY}`
3880
- ));
3881
- });
3882
-
3883
3743
  configCommand
3884
3744
  .command('init')
3885
3745
  .description('Pin this workspace to an environment (requires the global --env dev|demo|prod)')
@@ -4592,6 +4452,11 @@ program
4592
4452
  }
4593
4453
  });
4594
4454
 
4455
+ // ============ Retired verbs ============
4456
+ // Every retired verb is a row in lib/commands/retired-verbs.js, registered here in one call once
4457
+ // every parent exists. No retired verb name is typed in this file.
4458
+ registerAllRetiredVerbs({ program, kb: kbCommand, config: configCommand, microservice: microserviceCommand });
4459
+
4595
4460
  // ============ Public-vs-admin help listing (CEO ruling, see lib/command-visibility.js) ============
4596
4461
  //
4597
4462
  // The class comes from the server (`get_command_surface`, guest-allowed) at the moment help is
@@ -4650,6 +4515,10 @@ if (!adminMode && isHelpInvocation(program, rawArgv, { valueFlags: ['--env', '--
4650
4515
  }
4651
4516
  }
4652
4517
 
4518
+ // A command that does not exist must not appear to have help (see lib/help-guard.js). This runs
4519
+ // before the parse, because Commander resolves --help before it resolves the command.
4520
+ assertKnownCommandForHelp(program, process.argv.slice(2));
4521
+
4653
4522
  // Parse arguments
4654
4523
  await program.parseAsync(process.argv);
4655
4524
 
@@ -135,7 +135,7 @@ export function applyVisibility(program, surfaceCommands) {
135
135
  const hasChildren = Array.isArray(cmd.commands) && cmd.commands.length > 0;
136
136
  // ADDITIVE ONLY. Some commands are registered `{ hidden: true }` by the CLI itself for
137
137
  // reasons that have nothing to do with server admin-gating — see the retired-surface
138
- // registrations in lib/commands/retired-kb-sync.js, each a deliberate loud-refusal stub kept
138
+ // registrations in lib/commands/retired-verbs.js, each a deliberate loud-refusal stub kept
139
139
  // out of the default listing so it doesn't clutter it, while still working and refusing
140
140
  // clearly when typed. This layer may HIDE a visible command; it must never REVEAL a command
141
141
  // the CLI already chose to hide. MEASURED: an early version unconditionally overwrote
@@ -205,12 +205,17 @@ export async function executeQueue(options) {
205
205
  if (result.token_contract_address) console.log(` token_contract: ${result.token_contract_address}`);
206
206
  if (result.abi_source) console.log(` abi_source: ${result.abi_source.doc_id} (v${result.abi_source.version || '?'})`);
207
207
  console.log(` transfers_total: ${chalk.yellow(result.transfers_total)}`);
208
+ console.log(` batches_total: ${result.batches_total}`);
208
209
  console.log(` debit_total: ${result.debit_total}`);
209
210
  console.log(` credit_total: ${result.credit_total}`);
210
- console.log(` bonus_total: ${result.bonus_total}`);
211
211
  const nz = result.net_zero_assertion || {};
212
212
  const nzLabel = nz.passes ? chalk.green('PASS') : chalk.red('FAIL');
213
213
  console.log(` net_zero_assertion: ${nzLabel} (Σ=${nz.sum})`);
214
+ const pbc = result.pre_batch_checks || {};
215
+ console.log(` pre_batch_checks: ${pbc.passes ? chalk.green('PASS') : chalk.red('FAIL — --apply would refuse')}`);
216
+ for (const r of (pbc.refusals || [])) {
217
+ console.log(chalk.red(` ${r.kind} ${r.wallet} (${r.community_id}) holds ${r.held}, needs ${r.required} base units — rows ${(r.pending_ids || []).join(', ')}`));
218
+ }
214
219
  console.log(` unique_source_wallets: ${result.unique_source_wallets}`);
215
220
  console.log(` unique_master_wallets: ${result.unique_master_wallets}`);
216
221
  console.log(` prospective_batch_id: ${result.prospective_batch_id}`);
@@ -11,10 +11,10 @@ import { fileURLToPath } from 'url';
11
11
  import { dirname, join } from 'path';
12
12
  // clone.js is imported dynamically inside the invite flow to avoid circular deps
13
13
  import { WorkspaceConfig } from '../workspace-config.js';
14
- // The canonical KB-sync surface is owned by retired-kb-sync.js. Consume the constant: a literal
14
+ // The canonical KB-sync surface is owned by kb-sync-surface.js. Consume the constant: a literal
15
15
  // spelled here is a second derivation of the same fact, and the removed verb this replaced
16
16
  // reached a developer who had done everything right.
17
- import { CANONICAL_KB_SYNC } from './retired-kb-sync.js';
17
+ import { CANONICAL_KB_SYNC } from './kb-sync-surface.js';
18
18
  // "May I prompt?" has ONE OWNER. init does not derive it, and holds no TTY check of its own.
19
19
  import { createLazyPromptSession } from '../interactive.js';
20
20
 
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The canonical KB surfaces, named once. Every refusal, hint and next-step that points a
3
+ * developer at KB sync or KB creation consumes these constants rather than retyping them.
4
+ */
5
+
6
+ /** The single canonical KB sync surface. */
7
+ export const CANONICAL_KB_SYNC = 'descix kb corpus sync';
8
+
9
+ /** The verb that creates a KB — `kb corpus sync`'s dependency. */
10
+ export const CANONICAL_KB_CREATE = 'descix app init --kb <kb_name>';
@@ -32,7 +32,7 @@ import * as driveADC from '../google-storage-adc.js';
32
32
  // The one canonical KB-sync surface, from its owner. Never spell it as a literal here: these
33
33
  // commands' next steps are read by a developer as instructions, so they must name the live verb
34
34
  // and nothing else.
35
- import { CANONICAL_KB_SYNC } from './retired-kb-sync.js';
35
+ import { CANONICAL_KB_SYNC } from './kb-sync-surface.js';
36
36
  import { loadSyncState, syncStatePath } from '../core/syncState.js';
37
37
 
38
38
  // ============ Pull Command ============
@@ -0,0 +1,19 @@
1
+ /**
2
+ * ONE OWNER for how an app microservice authenticates, as the CLI states it.
3
+ *
4
+ * CEO-D-2026-06-02-APP-DATA-PLANE: an app microservice writes on the INJECTED CALLER auth
5
+ * (`params._descix`) with per-app_id isolation, never a per-service delegate key.
6
+ * `microservice register --help` prints this text, and the retired delegate-key verb's refusal
7
+ * (lib/commands/retired-verbs.js) prints it too.
8
+ */
9
+
10
+ export const MICROSERVICE_AUTH_RULING = 'CEO-D-2026-06-02-APP-DATA-PLANE';
11
+
12
+ /** The auth model, printed after `microservice register --help` and in the refusal. */
13
+ export const MICROSERVICE_AUTH_MODEL =
14
+ `A microservice is issued no key of its own (${MICROSERVICE_AUTH_RULING}).\n` +
15
+ ` Inbound: /apifront forwards each call with the caller's identity injected as params._descix,\n` +
16
+ ` isolated per app_id. Your handler acts on that caller.\n` +
17
+ ` Outbound: calls from your service to /apifront authenticate as the developer through\n` +
18
+ ` @descix/cli createServiceApiClient (DEVELOPER_WALLET_ADDRESS + DEVELOPER_SIGNATURE).\n` +
19
+ ` HTTP 401 on an outbound call: those two developer-credential values are missing or wrong.`;