xtrm-tools 0.7.13 → 0.7.15
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-agent-guard.mjs +76 -0
- package/.xtrm/registry.json +433 -413
- package/.xtrm/skills/default/releasing/SKILL.md +49 -45
- 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 +133 -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/update-specialists/SKILL.md +98 -392
- 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 +105 -15
- package/.xtrm/skills/default/using-xtrm/SKILL.md +14 -0
- package/CHANGELOG.md +22 -0
- package/README.md +5 -1
- package/cli/dist/index.cjs +2991 -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 +11 -0
- package/packages/pi-extensions/package.json +1 -1
- package/scripts/patch-external-pi-tools.mjs +154 -0
|
@@ -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>");
|