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.
- package/.xtrm/config/hooks.json +10 -0
- package/.xtrm/hooks/specialists/specialists-memory-cache-sync.mjs +57 -0
- package/.xtrm/hooks/specialists-agent-guard.mjs +76 -0
- package/.xtrm/registry.json +509 -393
- package/.xtrm/skills/default/premortem/SKILL.md +218 -0
- package/.xtrm/skills/default/releasing/SKILL.md +94 -0
- package/.xtrm/skills/default/releasing/scripts/xt-reports.ts +18 -0
- package/.xtrm/skills/default/session-close-report/SKILL.md +85 -17
- package/.xtrm/skills/default/specialists-creator/SKILL.md +117 -42
- package/.xtrm/skills/default/specialists-creator/scripts/audit-spec-uniformity.mjs +86 -0
- package/.xtrm/skills/default/specialists-creator/scripts/scaffold-specialist.ts +223 -0
- package/.xtrm/skills/default/specialists-creator/scripts/validate-specialist.ts +1 -1
- package/.xtrm/skills/default/sync-docs/SKILL.md +88 -208
- package/.xtrm/skills/default/sync-docs/scripts/pre-context.sh +17 -0
- package/.xtrm/skills/default/update-specialists/SKILL.md +99 -201
- package/.xtrm/skills/default/update-xt/SKILL.md +34 -0
- package/.xtrm/skills/default/using-kpi/SKILL.md +150 -0
- package/.xtrm/skills/default/using-nodes/SKILL.md +18 -102
- package/.xtrm/skills/default/using-script-specialists/SKILL.md +208 -0
- package/.xtrm/skills/default/using-specialists/SKILL.md +13 -0
- package/.xtrm/skills/default/using-specialists-v2/SKILL.md +773 -0
- package/.xtrm/skills/default/using-specialists-v3/SKILL.md +284 -0
- package/.xtrm/skills/default/using-specialists-v3/evals/evals.json +89 -0
- package/CHANGELOG.md +17 -0
- package/README.md +5 -1
- package/cli/dist/index.cjs +3401 -627
- package/cli/dist/index.cjs.map +1 -1
- package/cli/package.json +1 -1
- package/package.json +3 -2
- package/packages/pi-extensions/.serena/project.yml +130 -0
- package/packages/pi-extensions/extensions/pi-serena-compact/index.ts +4 -12
- package/packages/pi-extensions/extensions/xtrm-loader/index.ts +0 -1
- package/packages/pi-extensions/extensions/xtrm-ui/index.ts +201 -36
- package/packages/pi-extensions/extensions/xtrm-ui/themes/pidex-dark-flattools.json +79 -0
- package/packages/pi-extensions/extensions/xtrm-ui/themes/pidex-dark.json +85 -0
- package/packages/pi-extensions/extensions/xtrm-ui/themes/pidex-light-flattools.json +79 -0
- package/packages/pi-extensions/extensions/xtrm-ui/themes/pidex-light.json +85 -0
- package/packages/pi-extensions/package.json +1 -1
- package/packages/pi-extensions/themes/xtrm-ui/pidex-dark-flattools.json +79 -0
- package/packages/pi-extensions/themes/xtrm-ui/pidex-dark.json +3 -3
- package/packages/pi-extensions/themes/xtrm-ui/pidex-light-flattools.json +79 -0
- 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.
|
|
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
|
|
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/
|
|
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 |
|
|
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 | — |
|
|
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
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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/
|
|
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
|
|
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/
|
|
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/
|
|
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 "
|
|
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>");
|