xtrm-tools 0.7.12 → 0.7.14

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 (42) hide show
  1. package/.xtrm/config/hooks.json +10 -0
  2. package/.xtrm/hooks/specialists/specialists-memory-cache-sync.mjs +57 -0
  3. package/.xtrm/hooks/specialists-agent-guard.mjs +76 -0
  4. package/.xtrm/registry.json +509 -393
  5. package/.xtrm/skills/default/premortem/SKILL.md +218 -0
  6. package/.xtrm/skills/default/releasing/SKILL.md +94 -0
  7. package/.xtrm/skills/default/releasing/scripts/xt-reports.ts +18 -0
  8. package/.xtrm/skills/default/session-close-report/SKILL.md +85 -17
  9. package/.xtrm/skills/default/specialists-creator/SKILL.md +117 -42
  10. package/.xtrm/skills/default/specialists-creator/scripts/audit-spec-uniformity.mjs +86 -0
  11. package/.xtrm/skills/default/specialists-creator/scripts/scaffold-specialist.ts +223 -0
  12. package/.xtrm/skills/default/specialists-creator/scripts/validate-specialist.ts +1 -1
  13. package/.xtrm/skills/default/sync-docs/SKILL.md +88 -208
  14. package/.xtrm/skills/default/sync-docs/scripts/pre-context.sh +17 -0
  15. package/.xtrm/skills/default/update-specialists/SKILL.md +99 -201
  16. package/.xtrm/skills/default/update-xt/SKILL.md +34 -0
  17. package/.xtrm/skills/default/using-kpi/SKILL.md +150 -0
  18. package/.xtrm/skills/default/using-nodes/SKILL.md +18 -102
  19. package/.xtrm/skills/default/using-script-specialists/SKILL.md +208 -0
  20. package/.xtrm/skills/default/using-specialists/SKILL.md +13 -0
  21. package/.xtrm/skills/default/using-specialists-v2/SKILL.md +773 -0
  22. package/.xtrm/skills/default/using-specialists-v3/SKILL.md +284 -0
  23. package/.xtrm/skills/default/using-specialists-v3/evals/evals.json +89 -0
  24. package/CHANGELOG.md +17 -0
  25. package/README.md +5 -1
  26. package/cli/dist/index.cjs +3401 -627
  27. package/cli/dist/index.cjs.map +1 -1
  28. package/cli/package.json +1 -1
  29. package/package.json +3 -2
  30. package/packages/pi-extensions/.serena/project.yml +130 -0
  31. package/packages/pi-extensions/extensions/pi-serena-compact/index.ts +4 -12
  32. package/packages/pi-extensions/extensions/xtrm-loader/index.ts +0 -1
  33. package/packages/pi-extensions/extensions/xtrm-ui/index.ts +201 -36
  34. package/packages/pi-extensions/extensions/xtrm-ui/themes/pidex-dark-flattools.json +79 -0
  35. package/packages/pi-extensions/extensions/xtrm-ui/themes/pidex-dark.json +85 -0
  36. package/packages/pi-extensions/extensions/xtrm-ui/themes/pidex-light-flattools.json +79 -0
  37. package/packages/pi-extensions/extensions/xtrm-ui/themes/pidex-light.json +85 -0
  38. package/packages/pi-extensions/package.json +1 -1
  39. package/packages/pi-extensions/themes/xtrm-ui/pidex-dark-flattools.json +79 -0
  40. package/packages/pi-extensions/themes/xtrm-ui/pidex-dark.json +3 -3
  41. package/packages/pi-extensions/themes/xtrm-ui/pidex-light-flattools.json +79 -0
  42. package/scripts/patch-external-pi-tools.mjs +154 -0
@@ -5,7 +5,7 @@ description: >
5
5
  agent through writing a valid `.specialist.json`, choosing supported models,
6
6
  validating against the schema, and avoiding common specialist authoring
7
7
  mistakes.
8
- version: 1.1
8
+ version: 1.2
9
9
  synced_at: 236ca5e6
10
10
  ---
11
11
 
@@ -40,6 +40,7 @@ Model tiers:
40
40
  Rules:
41
41
  - Always pick the **highest version** in a family (`claude-sonnet-4-6` not `4-5`, `gemini-3.1-pro-preview` not `gemini-2.5-pro`)
42
42
  - `model` and `fallback_model` must be **different providers**
43
+ - If a specialist needs a longer fallback chain, keep first fallback in `fallback_model` and let runtime supply any extra retry tier.
43
44
  - Never write a model string you have not pinged in this session
44
45
 
45
46
  ---
@@ -162,6 +163,10 @@ specialists models # confirm assignments look balanced
162
163
 
163
164
  ---
164
165
 
166
+ ## Canonical references
167
+
168
+ Reference any canonical skill or rule by name; runtime finds it.
169
+
165
170
  ## Quick Start: Scaffold + `sp edit`
166
171
 
167
172
  ```bash
@@ -169,7 +174,7 @@ specialists models # confirm assignments look balanced
169
174
  node config/skills/specialists-creator/scripts/scaffold-specialist.ts config/specialists/my-specialist.specialist.json
170
175
 
171
176
  # 2. Apply a preset for common model/thinking defaults (optional but preferred)
172
- sp edit my-specialist --preset standard
177
+ sp edit my-specialist --preset medium
173
178
 
174
179
  # 3. Set individual fields via dot.path (primary mutation workflow)
175
180
  sp edit my-specialist specialist.metadata.name my-specialist
@@ -177,6 +182,8 @@ sp edit my-specialist specialist.metadata.version 1.0.0
177
182
  sp edit my-specialist specialist.execution.model anthropic/claude-sonnet-4-6
178
183
  sp edit my-specialist specialist.execution.fallback_model google-gemini-cli/gemini-3.1-pro-preview
179
184
  sp edit my-specialist specialist.execution.permission_required READ_ONLY
185
+ sp edit my-specialist specialist.execution.extensions.serena false
186
+ sp edit my-specialist specialist.execution.extensions.gitnexus false
180
187
 
181
188
  # 4. Use --file only for multiline prompt fields
182
189
  sp edit my-specialist specialist.prompt.system --file .tmp/system.prompt.txt
@@ -186,7 +193,7 @@ sp edit my-specialist specialist.prompt.task_template --file .tmp/task-template.
186
193
  sp view my-specialist
187
194
 
188
195
  # 6. Validate schema
189
- bun skills/specialist-author/scripts/validate-specialist.ts config/specialists/my-specialist.specialist.json
196
+ bun config/skills/specialists-creator/scripts/validate-specialist.ts config/specialists/my-specialist.specialist.json
190
197
  ```
191
198
 
192
199
  ---
@@ -199,19 +206,47 @@ bun skills/specialist-author/scripts/validate-specialist.ts config/specialists/m
199
206
  |-------|------|----------|-------|
200
207
  | `name` | string | yes | kebab-case: `[a-z][a-z0-9-]*` |
201
208
  | `version` | string | yes | semver: `1.0.0` |
202
- | `description` | string | yes | One sentence |
209
+ | `description` | string | yes | Routing summary surfaced by `specialists list`; see Description writing below |
203
210
  | `category` | string | yes | Free text (e.g. `workflow`, `analysis`, `codegen`) |
204
211
  | `author` | string | no | Optional |
205
212
  | `created` | string | no | Optional date |
206
213
  | `updated` | string | no | Optional date, quote it: `"2026-03-22"` |
207
214
  | `tags` | string[] | no | Optional list |
208
215
 
216
+
217
+ ### Description writing for `specialists list`
218
+
219
+ `specialist.metadata.description` is the routing surface that orchestrators see in `specialists list`. Write it as an operational role definition, not marketing copy. Keep the first clause distinctive because list output may truncate.
220
+
221
+ A good description answers, in this order:
222
+
223
+ 1. **Choose when** — the task shape that should route here.
224
+ 2. **Do not choose when** — adjacent roles that should win instead.
225
+ 3. **Distinctive capability** — what this specialist does that others do not.
226
+ 4. **Permission/risk note** — READ_ONLY/LOW/MEDIUM/HIGH implication when it affects orchestration.
227
+
228
+ Pattern:
229
+
230
+ ```text
231
+ <role noun>. Use for <specific task shape>. Not for <near misses>; use <better roles>. <permission/workflow distinction>.
232
+ ```
233
+
234
+ Examples:
235
+
236
+ ```text
237
+ Scoped implementation only. Use when requirements, files/symbols, constraints, and validation are clear. Not diagnosis, planning, review, tests, release, or research. HIGH worktree.
238
+
239
+ Debug symptoms/errors/regressions first. Use when cause is unknown or tests fail unexpectedly; traces, fixes targeted code, and verifies. HIGH keep-alive.
240
+ ```
241
+
242
+ Avoid vague descriptions like "general purpose assistant" or "helps with code". Those cause orchestrators to overuse familiar specialists instead of routing to debugger, test-runner, researcher, sync-docs, or other sharper roles.
243
+
209
244
  ### `specialist.execution` (required)
210
245
 
211
246
  | Field | Type | Default | Notes |
212
247
  |-------|------|---------|-------|
213
248
  | `model` | string | — | required — ping before using |
214
- | `fallback_model` | string | — | must be a different provider |
249
+ | `fallback_model` | string | — | first fallback only; runtime may append more tiers |
215
250
  | `mode` | enum | `auto` | `tool` \| `skill` \| `auto` |
216
251
  | `timeout_ms` | number | `120000` | ms |
217
252
  | `stall_timeout_ms` | number | — | kill if no event for N ms |
@@ -220,6 +255,8 @@ bun skills/specialist-author/scripts/validate-specialist.ts config/specialists/m
220
255
  | `output_type` | enum | `custom` | `codegen` \| `analysis` \| `review` \| `synthesis` \| `orchestration` \| `workflow` \| `research` \| `custom` |
221
256
  | `permission_required` | enum | `READ_ONLY` | see tier table below |
222
257
  | `thinking_level` | enum | — | `off` \| `minimal` \| `low` \| `medium` \| `high` \| `xhigh` |
258
+ | `extensions.serena` | boolean | `true` | set `false` to opt out of Serena extension injection for this specialist |
259
+ | `extensions.gitnexus` | boolean | `true` | set `false` to opt out of GitNexus extension injection for this specialist |
223
260
 
224
261
  **When to use `execution.interactive`**
225
262
 
@@ -230,17 +267,81 @@ bun skills/specialist-author/scripts/validate-specialist.ts config/specialists/m
230
267
  - MCP `start_specialist`: `keep_alive` enables, `no_keep_alive` disables.
231
268
  - Effective precedence: explicit disable (`--no-keep-alive` / `no_keep_alive`) → explicit enable (`--keep-alive` / `keep_alive`) → `execution.interactive` → one-shot default.
232
269
 
233
- **Permission tiers** — controls which pi tools are available:
270
+ **Permission tiers** — controls the *native* pi tools the specialist gets. The full resolved tool set also includes catalog-defined GitNexus and Serena tools per tier; see [docs/manifest.md](../../../docs/manifest.md) for the complete picture.
271
+
272
+ | Level | Native tools (cumulative) | Use when |
273
+ |-------|---------------------------|----------|
274
+ | `READ_ONLY` | `read, grep, find, ls` | Read-only analysis, no bash |
275
+ | `LOW` | `+ bash` | Inspect/run commands, no file edits |
276
+ | `MEDIUM` | `+ edit` | Can edit existing files |
277
+ | `HIGH` | `+ write` | Full access — can create new files |
234
278
 
235
- | Level | pi --tools | Use when |
236
- |-------|-----------|----------|
237
- | `READ_ONLY` | `read,grep,find,ls` | Read-only analysis, no bash |
238
- | `LOW` | `+bash` | Inspect/run commands, no file edits |
239
- | `MEDIUM` | `+edit` | Can edit existing files |
240
- | `HIGH` | `+write` | Full access — can create new files |
279
+ After choosing a tier, verify the resolved tool list before dispatching:
280
+
281
+ ```bash
282
+ sp config show <name> --resolved
283
+ ```
241
284
 
242
285
  **Common pitfall:** `READ_WRITE` is **not** a valid value — use `LOW` or higher.
243
286
 
287
+ ### Per-specialist `permissions[<TIER>]` override (rarely needed)
288
+
289
+ Most specialists use the catalog default deny baseline. **Do not declare an override unless this specialist's policy genuinely diverges from its tier.** When you do override, remember the specialist block replaces catalog defaults for that tier.
290
+
291
+ If divergence is real, add a top-level `permissions` block (sibling to `execution`):
292
+
293
+ ```jsonc
294
+ {
295
+ "specialist": {
296
+ "execution": { "permission_required": "READ_ONLY" },
297
+ "permissions": {
298
+ "READ_ONLY": {
299
+ "denied_natives_when_extension": ["grep", "find", "ls"],
300
+ "denied_natives_mode": "hard"
301
+ }
302
+ }
303
+ }
304
+ }
305
+ ```
306
+
307
+ | Field | Type | Default | Effect |
308
+ |-------|------|---------|--------|
309
+ | `denied_natives_when_extension` | `string[]` | `[]` | Native tools to deny only when a replacement extension is healthy. Catalog defaults apply first; specialist override replaces them for that tier. |
310
+ | `denied_natives_mode` | `"soft"` \| `"hard"` | `"soft"` | `soft` keeps the tool with a preference hint; `hard` removes it (with auto-restore if the extension degrades) |
311
+
312
+ The override block can only *deny* natives — it cannot add new tools beyond the catalog tier. To add tools, change the tier or update the catalog file.
313
+
314
+ **Decision rule when authoring:**
315
+ 1. Pick the lowest tier that satisfies the specialist's actual capability needs.
316
+ 2. Run `sp config show <name> --resolved` and inspect the `--tools` line.
317
+ 3. If the tools are right, you're done — no override needed.
318
+ 4. If a native tool is genuinely worse than an extension equivalent for this specialist's task, declare a soft-deny first to observe behavior, then promote to hard-deny once you trust it.
319
+
320
+ See [docs/manifest.md](../../../docs/manifest.md) for full deny-mode semantics, extension health gating, and the canonical explorer example.
321
+
322
+ **Per-specialist extension opt-out**
323
+
324
+ Use `execution.extensions` only when this specialist must suppress default extension injection.
325
+ Both flags default to `true`, so omit this block unless opt-out is required.
326
+
327
+ ```json
328
+ {
329
+ "specialist": {
330
+ "execution": {
331
+ "extensions": {
332
+ "serena": false,
333
+ "gitnexus": false
334
+ }
335
+ }
336
+ }
337
+ }
338
+ ```
339
+
340
+ Typical use cases:
341
+ - `serena: false` for specialists that must avoid Serena tool/LSP injection
342
+ - `gitnexus: false` for specialists that should not receive GitNexus graph tooling
343
+ - set both `false` for constrained runs that need clean extension surface
344
+
244
345
  ### `specialist.prompt` (required)
245
346
 
246
347
  | Field | Type | Required | Notes |
@@ -356,8 +457,6 @@ planner — epic result:
356
457
 
357
458
  `run` accepts either a **file path** (`./scripts/foo.sh`, `~/scripts/foo.sh`) or a **shell command** (`bd ready`, `git status`). Pre-run validation checks that file paths exist and shell commands are on `PATH`. Shebang typos (e.g. `pytho` instead of `python`) are caught and reported as errors before the session starts.
358
459
 
359
- `path` is accepted as a deprecated alias for `run`.
360
-
361
460
  ### `specialist.capabilities` (optional)
362
461
 
363
462
  Informational declarations used by pre-run validation and future tooling (e.g. `specialists doctor`).
@@ -383,27 +482,6 @@ Informational declarations used by pre-run validation and future tooling (e.g. `
383
482
 
384
483
  Writes the final session output to this file path after the session completes. Relative to the working directory.
385
484
 
386
- ### `specialist.communication` (optional)
387
-
388
- ```json
389
- {
390
- "communication": {
391
- "next_specialists": "planner"
392
- }
393
- }
394
- ```
395
-
396
- Or as an array:
397
- ```json
398
- {
399
- "communication": {
400
- "next_specialists": ["planner", "test-runner"]
401
- }
402
- }
403
- ```
404
-
405
- `next_specialists` declares which specialist(s) should receive this specialist's output as `$previous_result`. Chaining is executed by the caller (e.g. `run_parallel` pipeline) — this field is declarative metadata.
406
-
407
485
  ### `specialist.validation` (optional)
408
486
 
409
487
  Drives the staleness detection shown in `specialists status` and `specialists list`.
@@ -480,7 +558,7 @@ Files listed under `skills.paths` are read and appended to the system prompt at
480
558
  {
481
559
  "skills": {
482
560
  "paths": [
483
- "skills/specialist-author/SKILL.md",
561
+ ".xtrm/skills/active/specialists-creator/SKILL.md",
484
562
  ".claude/agents.md"
485
563
  ]
486
564
  }
@@ -576,9 +654,6 @@ Scripts run **locally** (not inside the agent session):
576
654
  "required_tools": ["bash", "read"],
577
655
  "external_commands": ["git"]
578
656
  },
579
- "communication": {
580
- "next_specialists": ["sync-docs"]
581
- },
582
657
  "output_file": ".specialists/review.md",
583
658
  "beads_integration": "auto"
584
659
  }
@@ -681,7 +756,7 @@ pi --model <provider>/<fallback-model-id> --print "ping" # must return "pong"
681
756
  node config/skills/specialists-creator/scripts/scaffold-specialist.ts config/specialists/my-specialist.specialist.json
682
757
 
683
758
  # 3. Mutate with sp edit (dot.path + presets)
684
- sp edit my-specialist --preset standard
759
+ sp edit my-specialist --preset medium
685
760
  sp edit my-specialist specialist.execution.model <provider>/<primary-model-id>
686
761
  sp edit my-specialist specialist.execution.fallback_model <provider>/<fallback-model-id>
687
762
 
@@ -693,7 +768,7 @@ sp edit my-specialist specialist.prompt.task_template --file .tmp/task-template.
693
768
  sp view my-specialist
694
769
 
695
770
  # 6. Validate schema with the bundled helper
696
- bun skills/specialist-author/scripts/validate-specialist.ts config/specialists/my-specialist.specialist.json
771
+ bun config/skills/specialists-creator/scripts/validate-specialist.ts config/specialists/my-specialist.specialist.json
697
772
 
698
773
  # 7. List to confirm discovery
699
774
  specialists list
@@ -702,4 +777,4 @@ specialists list
702
777
  specialists run my-specialist --prompt "ping" --no-beads
703
778
  ```
704
779
 
705
- If you need the underlying implementation, read `skills/specialist-author/scripts/validate-specialist.ts`. It is a thin Bun/TypeScript wrapper over `parseSpecialist()` from `src/specialist/schema.ts`, which keeps the helper cross-platform for Windows, macOS, and Linux.
780
+ If you need the underlying implementation, read `config/skills/specialists-creator/scripts/validate-specialist.ts`. It is a thin Bun/TypeScript wrapper over `parseSpecialist()` from `src/specialist/schema.ts`, which keeps the helper cross-platform for Windows, macOS, and Linux.
@@ -0,0 +1,86 @@
1
+ // Audit every .specialist.json under config/specialists/ and .specialists/default/
2
+ // for: (1) schema parse failures, (2) unknown keys that survive .passthrough() silently.
3
+ //
4
+ // Usage (from repo root): bun config/skills/specialists-creator/scripts/audit-spec-uniformity.mjs
5
+ //
6
+ // Keep KNOWN sets in sync with src/specialist/schema.ts. If a sub-schema gains
7
+ // or drops a field, update this file in the same commit.
8
+
9
+ import { readFileSync, readdirSync } from 'node:fs';
10
+ import { join, resolve } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ const here = fileURLToPath(import.meta.url);
14
+ const repoRoot = resolve(here, '../../../../..');
15
+ const { validateSpecialist } = await import(resolve(repoRoot, 'src/specialist/schema.ts'));
16
+
17
+ // Walk known schema keys to detect "unknown" passthrough survivors
18
+ const KNOWN = {
19
+ root: new Set(['specialist']),
20
+ specialist: new Set(['metadata','execution','prompt','skills','capabilities','communication','validation','beads_integration','beads_write_notes','stall_detection','heartbeat','mandatory_rules','output_file']),
21
+ metadata: new Set(['name','version','description','category','author','created','updated','tags']),
22
+ execution: new Set(['mode','model','fallback_model','timeout_ms','stall_timeout_ms','max_retries','interactive','response_format','output_type','permission_required','requires_worktree','thinking_level','auto_commit','extensions','preferred_profile','approval_mode']),
23
+ 'execution.extensions': new Set(['serena','gitnexus']),
24
+ prompt: new Set(['system','task_template','normalize_template','output_schema','examples','skill_inherit']),
25
+ skills: new Set(['paths','scripts']),
26
+ 'skills.scripts.item': new Set(['run','path','phase','inject_output']),
27
+ capabilities: new Set(['required_tools','external_commands','diagnostic_scripts']),
28
+ communication: new Set(['next_specialists','publishes']),
29
+ validation: new Set(['files_to_watch','stale_threshold_days']),
30
+ stall_detection: new Set(['running_idle_warn_ms','running_idle_kill_ms','waiting_stale_ms','tool_duration_warn_ms']),
31
+ };
32
+
33
+ function unknownKeys(obj, knownSet, path) {
34
+ const out = [];
35
+ for (const k of Object.keys(obj || {})) if (!knownSet.has(k)) out.push(`${path}.${k}`);
36
+ return out;
37
+ }
38
+
39
+ function audit(file) {
40
+ const raw = JSON.parse(readFileSync(file,'utf8'));
41
+ const findings = [];
42
+ // raw key check (before parse strips/preserves)
43
+ findings.push(...unknownKeys(raw, KNOWN.root, ''));
44
+ const s = raw.specialist ?? {};
45
+ findings.push(...unknownKeys(s, KNOWN.specialist, 'specialist'));
46
+ if (s.metadata) findings.push(...unknownKeys(s.metadata, KNOWN.metadata, 'specialist.metadata'));
47
+ if (s.execution) findings.push(...unknownKeys(s.execution, KNOWN.execution, 'specialist.execution'));
48
+ if (s.execution?.extensions) findings.push(...unknownKeys(s.execution.extensions, KNOWN['execution.extensions'], 'specialist.execution.extensions'));
49
+ if (s.prompt) findings.push(...unknownKeys(s.prompt, KNOWN.prompt, 'specialist.prompt'));
50
+ if (s.skills) findings.push(...unknownKeys(s.skills, KNOWN.skills, 'specialist.skills'));
51
+ if (Array.isArray(s.skills?.scripts)) for (const [i, sc] of s.skills.scripts.entries()) findings.push(...unknownKeys(sc, KNOWN['skills.scripts.item'], `specialist.skills.scripts[${i}]`));
52
+ if (s.capabilities) findings.push(...unknownKeys(s.capabilities, KNOWN.capabilities, 'specialist.capabilities'));
53
+ if (s.communication) findings.push(...unknownKeys(s.communication, KNOWN.communication, 'specialist.communication'));
54
+ if (s.validation) findings.push(...unknownKeys(s.validation, KNOWN.validation, 'specialist.validation'));
55
+ if (s.stall_detection) findings.push(...unknownKeys(s.stall_detection, KNOWN.stall_detection, 'specialist.stall_detection'));
56
+ return findings;
57
+ }
58
+
59
+ const files = [
60
+ ...readdirSync(resolve(repoRoot,'config/specialists')).filter(f=>f.endsWith('.specialist.json')).map(f=>join(repoRoot,'config/specialists',f)),
61
+ ...readdirSync(resolve(repoRoot,'.specialists/default')).filter(f=>f.endsWith('.specialist.json')).map(f=>join(repoRoot,'.specialists/default',f)),
62
+ ];
63
+
64
+ let totalUnknown = 0;
65
+ let parseErrors = 0;
66
+ for (const file of files) {
67
+ try {
68
+ const v = await validateSpecialist(readFileSync(file,'utf8'));
69
+ if (!v.valid) {
70
+ parseErrors++;
71
+ console.log(`\n✗ PARSE FAIL ${file}`);
72
+ for (const e of v.errors) console.log(` ${e.path}: ${e.message}`);
73
+ continue;
74
+ }
75
+ const unk = audit(file);
76
+ if (unk.length) {
77
+ totalUnknown += unk.length;
78
+ console.log(`\n⚠ ${file}`);
79
+ for (const k of unk) console.log(` unknown key: ${k}`);
80
+ }
81
+ } catch (e) {
82
+ parseErrors++;
83
+ console.log(`\n✗ ERROR ${file}: ${e.message}`);
84
+ }
85
+ }
86
+ console.log(`\n=== ${files.length} specs · ${parseErrors} parse errors · ${totalUnknown} unknown keys ===`);
@@ -0,0 +1,223 @@
1
+ import { readFileSync, readdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import * as z from "zod";
4
+ import { SpecialistSchema } from "../../../../src/specialist/schema.ts";
5
+
6
+ const DEAD_FIELDS = new Set<string>([]);
7
+
8
+ interface AddedField {
9
+ path: string;
10
+ value: unknown;
11
+ }
12
+
13
+ interface ScaffoldResult {
14
+ value: unknown;
15
+ added: AddedField[];
16
+ changed: boolean;
17
+ }
18
+
19
+ function printUsage(): void {
20
+ console.error("Usage: node scripts/scaffold-specialist.ts <path-to-specialist.json>");
21
+ console.error(" or: node scripts/scaffold-specialist.ts --all");
22
+ }
23
+
24
+ function unwrapSchema(schema: z.ZodTypeAny): z.ZodTypeAny {
25
+ let current = schema;
26
+ while (
27
+ current instanceof z.ZodOptional ||
28
+ current instanceof z.ZodNullable ||
29
+ current instanceof z.ZodDefault ||
30
+ current instanceof z.ZodEffects
31
+ ) {
32
+ if (current instanceof z.ZodEffects) {
33
+ current = current.innerType();
34
+ continue;
35
+ }
36
+
37
+ if (current instanceof z.ZodDefault) {
38
+ current = current._def.innerType;
39
+ continue;
40
+ }
41
+
42
+ current = current.unwrap();
43
+ }
44
+ return current;
45
+ }
46
+
47
+ function isOptionalWithoutDefault(schema: z.ZodTypeAny): boolean {
48
+ if (schema instanceof z.ZodOptional) {
49
+ return true;
50
+ }
51
+ if (schema instanceof z.ZodNullable) {
52
+ return isOptionalWithoutDefault(schema.unwrap());
53
+ }
54
+ if (schema instanceof z.ZodEffects) {
55
+ return isOptionalWithoutDefault(schema.innerType());
56
+ }
57
+ if (schema instanceof z.ZodDefault) {
58
+ return false;
59
+ }
60
+ return false;
61
+ }
62
+
63
+ function isRecord(value: unknown): value is Record<string, unknown> {
64
+ return typeof value === "object" && value !== null && !Array.isArray(value);
65
+ }
66
+
67
+ function formatValue(value: unknown): string {
68
+ if (typeof value === "string") {
69
+ return JSON.stringify(value);
70
+ }
71
+ return JSON.stringify(value);
72
+ }
73
+
74
+ function scaffoldSchema(schema: z.ZodTypeAny, currentValue: unknown, path: string[]): ScaffoldResult {
75
+ if (schema instanceof z.ZodEffects) {
76
+ return scaffoldSchema(schema.innerType(), currentValue, path);
77
+ }
78
+
79
+ if (schema instanceof z.ZodDefault) {
80
+ const inner = schema._def.innerType;
81
+ if (currentValue === undefined) {
82
+ const defaultValue = schema._def.defaultValue();
83
+ const nested = scaffoldSchema(inner, defaultValue, path);
84
+ return {
85
+ value: nested.value,
86
+ added: [{ path: path.join("."), value: nested.value }, ...nested.added],
87
+ changed: true,
88
+ };
89
+ }
90
+ return scaffoldSchema(inner, currentValue, path);
91
+ }
92
+
93
+ if (schema instanceof z.ZodOptional) {
94
+ if (currentValue === undefined) {
95
+ return { value: currentValue, added: [], changed: false };
96
+ }
97
+ return scaffoldSchema(schema.unwrap(), currentValue, path);
98
+ }
99
+
100
+ if (schema instanceof z.ZodNullable) {
101
+ if (currentValue === null || currentValue === undefined) {
102
+ return { value: currentValue, added: [], changed: false };
103
+ }
104
+ return scaffoldSchema(schema.unwrap(), currentValue, path);
105
+ }
106
+
107
+ if (schema instanceof z.ZodArray) {
108
+ if (currentValue === undefined) {
109
+ return {
110
+ value: [],
111
+ added: [{ path: path.join("."), value: [] }],
112
+ changed: true,
113
+ };
114
+ }
115
+ return { value: currentValue, added: [], changed: false };
116
+ }
117
+
118
+ if (schema instanceof z.ZodEnum) {
119
+ return { value: currentValue, added: [], changed: false };
120
+ }
121
+
122
+ if (schema instanceof z.ZodObject) {
123
+ const source = isRecord(currentValue) ? currentValue : undefined;
124
+ const draft: Record<string, unknown> = source ? { ...source } : {};
125
+ const added: AddedField[] = [];
126
+ let changed = false;
127
+
128
+ const shape = schema.shape;
129
+ for (const [key, childSchema] of Object.entries(shape)) {
130
+ if (DEAD_FIELDS.has(key)) {
131
+ continue;
132
+ }
133
+
134
+ const childPath = [...path, key];
135
+ const childValue = source?.[key];
136
+ const childResult = scaffoldSchema(childSchema as z.ZodTypeAny, childValue, childPath);
137
+
138
+ if (!childResult.changed) {
139
+ continue;
140
+ }
141
+
142
+ draft[key] = childResult.value;
143
+ added.push(...childResult.added);
144
+ changed = true;
145
+ }
146
+
147
+ if (!source) {
148
+ if (!changed || isOptionalWithoutDefault(schema)) {
149
+ return { value: currentValue, added, changed: false };
150
+ }
151
+ return { value: draft, added, changed: true };
152
+ }
153
+
154
+ return { value: changed ? draft : currentValue, added, changed };
155
+ }
156
+
157
+ const unwrapped = unwrapSchema(schema);
158
+ if (
159
+ unwrapped instanceof z.ZodString ||
160
+ unwrapped instanceof z.ZodNumber ||
161
+ unwrapped instanceof z.ZodBoolean
162
+ ) {
163
+ return { value: currentValue, added: [], changed: false };
164
+ }
165
+
166
+ return { value: currentValue, added: [], changed: false };
167
+ }
168
+
169
+ function loadTargets(arg: string): string[] {
170
+ if (arg !== "--all") {
171
+ return [arg];
172
+ }
173
+
174
+ const specialistsDir = join(process.cwd(), "config", "specialists");
175
+ return readdirSync(specialistsDir)
176
+ .filter(file => file.endsWith(".specialist.json"))
177
+ .sort()
178
+ .map(file => join(specialistsDir, file));
179
+ }
180
+
181
+ function processFile(filePath: string): AddedField[] {
182
+ const raw = readFileSync(filePath, "utf8");
183
+ const parsed = JSON.parse(raw);
184
+
185
+ if (!isRecord(parsed)) {
186
+ throw new Error(`Expected JSON object in ${filePath}`);
187
+ }
188
+
189
+ const result = scaffoldSchema(SpecialistSchema, parsed, []);
190
+ if (!result.changed) {
191
+ return [];
192
+ }
193
+
194
+ writeFileSync(filePath, `${JSON.stringify(result.value, null, 2)}\n`, "utf8");
195
+ return result.added;
196
+ }
197
+
198
+ function run(): void {
199
+ const targetArg = process.argv[2];
200
+ if (!targetArg) {
201
+ printUsage();
202
+ process.exit(64);
203
+ }
204
+
205
+ const targets = loadTargets(targetArg);
206
+ if (targets.length === 0) {
207
+ console.log("No specialist files found.");
208
+ return;
209
+ }
210
+
211
+ for (const filePath of targets) {
212
+ const addedFields = processFile(filePath);
213
+ if (addedFields.length === 0) {
214
+ continue;
215
+ }
216
+
217
+ for (const field of addedFields) {
218
+ console.log(`${filePath}: ${field.path} = ${formatValue(field.value)}`);
219
+ }
220
+ }
221
+ }
222
+
223
+ run();
@@ -1,5 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { parseSpecialist } from "../../../src/specialist/schema.ts";
2
+ import { parseSpecialist } from "../../../../src/specialist/schema.ts";
3
3
 
4
4
  function printUsage(): void {
5
5
  console.error("Usage: bun skills/specialist-author/scripts/validate-specialist.ts <path-to.specialist.yaml>");