@dzhechkov/harness-core 0.3.18 → 0.3.20
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/dist/benchmark.d.ts +3 -0
- package/dist/benchmark.d.ts.map +1 -1
- package/dist/benchmark.js +3 -2
- package/dist/benchmark.js.map +1 -1
- package/dist/cost-scoring.d.ts +119 -0
- package/dist/cost-scoring.d.ts.map +1 -0
- package/dist/cost-scoring.js +191 -0
- package/dist/cost-scoring.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/recommend.d.ts +7 -2
- package/dist/recommend.d.ts.map +1 -1
- package/dist/recommend.js +37 -6
- package/dist/recommend.js.map +1 -1
- package/dist/setup.d.ts +2 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +122 -0
- package/dist/setup.js.map +1 -1
- package/dist/sync-upstream.d.ts.map +1 -1
- package/dist/sync-upstream.js +7 -1
- package/dist/sync-upstream.js.map +1 -1
- package/package.json +1 -1
- package/src/benchmark.ts +6 -2
- package/src/cost-scoring.ts +273 -0
- package/src/index.ts +17 -0
- package/src/recommend.ts +32 -6
- package/src/setup.ts +127 -0
- package/src/sync-upstream.ts +5 -1
package/src/recommend.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import type { Registry, RegistryEntry } from './registry.js';
|
|
17
|
+
import { pretrain } from './pretrain.js';
|
|
17
18
|
|
|
18
19
|
/** A recommended skill with relevance score. */
|
|
19
20
|
export interface SkillRecommendation {
|
|
@@ -60,13 +61,15 @@ export interface RecommendationReport {
|
|
|
60
61
|
readonly commands: readonly CommandRecommendation[];
|
|
61
62
|
readonly installCommand: string;
|
|
62
63
|
readonly plan: readonly string[];
|
|
64
|
+
/** Set when task was too generic and pretrain was used as fallback. */
|
|
65
|
+
readonly pretrainFallback?: boolean;
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
/** Topic → keywords mapping for task decomposition. */
|
|
66
69
|
const TOPIC_KEYWORDS: Record<string, string[]> = {
|
|
67
70
|
'api': ['api', 'rest', 'graphql', 'endpoint', 'openapi', 'swagger', 'http', 'grpc'],
|
|
68
71
|
'testing': ['test', 'testing', 'tdd', 'unit test', 'integration test', 'e2e', 'coverage', 'spec'],
|
|
69
|
-
'ci-cd': ['ci', 'cd', 'pipeline', 'github actions', 'gitlab', 'jenkins', 'deploy', '
|
|
72
|
+
'ci-cd': ['ci/cd', 'ci cd', 'pipeline', 'github actions', 'gitlab', 'jenkins', 'deploy', 'continuous integration', 'continuous delivery'],
|
|
70
73
|
'security': ['security', 'audit', 'vulnerability', 'owasp', 'injection', 'auth', 'codeql', 'sast'],
|
|
71
74
|
'database': ['database', 'migration', 'schema', 'sql', 'postgres', 'mysql', 'query', 'index'],
|
|
72
75
|
'kubernetes': ['kubernetes', 'k8s', 'helm', 'pod', 'deployment', 'container', 'cluster', 'service mesh'],
|
|
@@ -75,7 +78,7 @@ const TOPIC_KEYWORDS: Record<string, string[]> = {
|
|
|
75
78
|
'monitoring': ['monitoring', 'observability', 'metrics', 'logs', 'traces', 'alerting', 'slo', 'grafana', 'prometheus'],
|
|
76
79
|
'incident': ['incident', 'outage', 'postmortem', 'oncall', 'pagerduty', 'sev1', 'downtime'],
|
|
77
80
|
'monorepo': ['monorepo', 'workspace', 'pnpm', 'turborepo', 'nx', 'changeset', 'lerna'],
|
|
78
|
-
'review': ['review', '
|
|
81
|
+
'review': ['review', 'pull request', 'code review', 'merge', ' pr ', 'pr '],
|
|
79
82
|
'debug': ['debug', 'error', 'crash', 'stack trace', 'bug', 'fix', 'troubleshoot'],
|
|
80
83
|
'frontend': ['frontend', 'react', 'vue', 'component', 'ui', 'css', 'tailwind'],
|
|
81
84
|
'git': ['git', 'merge', 'rebase', 'conflict', 'branch', 'cherry-pick'],
|
|
@@ -167,9 +170,32 @@ function scoreSkill(entry: RegistryEntry, topics: string[]): number {
|
|
|
167
170
|
return score;
|
|
168
171
|
}
|
|
169
172
|
|
|
170
|
-
/** Generate recommendation from a task and registry.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
+
/** Generate recommendation from a task and registry.
|
|
174
|
+
* When task is too generic (only 'general' topic), falls back to pretrain
|
|
175
|
+
* to analyze the actual project and recommend based on tech stack.
|
|
176
|
+
*/
|
|
177
|
+
export function recommend(task: string, registry: Registry, projectRoot?: string): RecommendationReport {
|
|
178
|
+
let topics = extractTopics(task);
|
|
179
|
+
let pretrainFallback = false;
|
|
180
|
+
|
|
181
|
+
// Fallback: if task is too generic, use pretrain to detect project stack
|
|
182
|
+
if (topics.length === 1 && topics[0] === 'general' && projectRoot) {
|
|
183
|
+
const analysis = pretrain(projectRoot);
|
|
184
|
+
const pretrainTopics: string[] = [];
|
|
185
|
+
const techNames = analysis.techs.map((t) => t.name.toLowerCase());
|
|
186
|
+
if (techNames.some((t) => t.includes('node') || t.includes('typescript'))) pretrainTopics.push('api', 'testing');
|
|
187
|
+
if (techNames.some((t) => t.includes('python') || t.includes('django') || t.includes('fastapi'))) pretrainTopics.push('api', 'testing');
|
|
188
|
+
if (techNames.some((t) => t.includes('react') || t.includes('vue') || t.includes('angular'))) pretrainTopics.push('frontend');
|
|
189
|
+
if (analysis.hasDocker) pretrainTopics.push('docker');
|
|
190
|
+
if (analysis.hasTerraform) pretrainTopics.push('terraform');
|
|
191
|
+
if (analysis.hasKubernetes) pretrainTopics.push('kubernetes');
|
|
192
|
+
if (analysis.hasCI) pretrainTopics.push('ci-cd');
|
|
193
|
+
if (analysis.hasTests) pretrainTopics.push('testing');
|
|
194
|
+
if (pretrainTopics.length > 0) {
|
|
195
|
+
topics = [...new Set(pretrainTopics)];
|
|
196
|
+
pretrainFallback = true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
173
199
|
|
|
174
200
|
// Score and rank skills
|
|
175
201
|
const scored = registry.entries
|
|
@@ -252,5 +278,5 @@ export function recommend(task: string, registry: Registry): RecommendationRepor
|
|
|
252
278
|
}
|
|
253
279
|
plan.push(`${plan.length + 1}. Use your agent normally — skills auto-activate on matching tasks`);
|
|
254
280
|
|
|
255
|
-
return { task, topics, skills, presets, toolkits, commands, installCommand, plan };
|
|
281
|
+
return { task, topics, skills, presets, toolkits, commands, installCommand, plan, pretrainFallback };
|
|
256
282
|
}
|
package/src/setup.ts
CHANGED
|
@@ -29,6 +29,8 @@ export interface SetupOptions {
|
|
|
29
29
|
readonly noMemory?: boolean | undefined;
|
|
30
30
|
readonly noPretrain?: boolean | undefined;
|
|
31
31
|
readonly force?: boolean | undefined;
|
|
32
|
+
/** Also deploy the operating-instructions "driver" skill + agent docs. */
|
|
33
|
+
readonly installDriver?: boolean | undefined;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
/** Setup result. */
|
|
@@ -122,6 +124,125 @@ function isAgentdbAvailable(): boolean {
|
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
/** Run full environment setup. */
|
|
127
|
+
/** Marker that brackets the dz-harness section in a shared CLAUDE.md/AGENTS.md. */
|
|
128
|
+
const DRIVER_MARKER_START = '<!-- dz-harness-driver:start -->';
|
|
129
|
+
const DRIVER_MARKER_END = '<!-- dz-harness-driver:end -->';
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Operating instructions for a coding agent that should *drive* the dz CLI
|
|
133
|
+
* correctly. Inspired by Visa VVAH `--install-agents`: the toolkit ships its own
|
|
134
|
+
* "how to operate me" doc so agents use the CLI as intended rather than guessing.
|
|
135
|
+
*/
|
|
136
|
+
function generateDriverInstructions(): string {
|
|
137
|
+
return `# Operating dz-harness-hub (CLI driver)
|
|
138
|
+
|
|
139
|
+
You have the \`dz\` CLI (\`@dzhechkov/harness-cli\`) available. It manages cross-platform
|
|
140
|
+
AI skills (the agentskills.io \`SKILL.md\` format) across Claude Code, Codex, OpenCode,
|
|
141
|
+
Hermes, and OpenClaude.
|
|
142
|
+
|
|
143
|
+
## Core commands
|
|
144
|
+
|
|
145
|
+
| Command | Use it when |
|
|
146
|
+
|---------|-------------|
|
|
147
|
+
| \`dz recommend "<task>"\` | The user describes a task — suggests the right skills/preset/npx package. Start here. |
|
|
148
|
+
| \`dz setup --target <t> [--preset <p>] [--memory agentdb]\` | Bootstrap a project: config, hooks, learning memory. |
|
|
149
|
+
| \`dz init --target <t> [--preset <p>] [--select id,id]\` | Install skills into a project for a platform. |
|
|
150
|
+
| \`dz benchmark <skill-dir>\` | Score a skill (L0 structural checks, grade A–F, cost band). |
|
|
151
|
+
| \`dz pretrain\` | Detect the project's tech stack and pre-load relevant skills. |
|
|
152
|
+
| \`dz scout [--deep]\` | Discover new skill sources across the ecosystem. |
|
|
153
|
+
| \`dz import-ecc\` | Import skills from an ECC repo. |
|
|
154
|
+
|
|
155
|
+
## Rules for driving this CLI
|
|
156
|
+
|
|
157
|
+
1. **Do NOT hand-edit \`SKILL.md\` files to make a benchmark pass** — fix the underlying
|
|
158
|
+
structure (missing frontmatter, sections, schema) instead.
|
|
159
|
+
2. **Do NOT hand-write \`.dz/config.json\`** — run \`dz setup\` and let it generate config.
|
|
160
|
+
3. **Skills are agentskills.io format** — YAML frontmatter (name, description, trust_tier,
|
|
161
|
+
validation) + a Markdown body with a Protocol and Anti-Patterns section.
|
|
162
|
+
4. **Presets bundle skills**; prefer \`--preset\` over selecting individual skills unless the
|
|
163
|
+
user wants a minimal install.
|
|
164
|
+
5. **Trust tiers**: tier 1 (Structured) → run \`/bto-test\` to promote to tier 2 (Validated).
|
|
165
|
+
6. When unsure which skills fit, run \`dz recommend\` first and follow its output.
|
|
166
|
+
|
|
167
|
+
## Targets (platform install dirs)
|
|
168
|
+
|
|
169
|
+
claude-code → \`.claude/skills/\` · codex → \`.agents/skills/\` · opencode → \`.opencode/\` ·
|
|
170
|
+
hermes → \`.hermes/\` · openclaude → \`.claude/skills/\`. The SKILL.md format is identical
|
|
171
|
+
across all five — only the directory differs.
|
|
172
|
+
`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The same instructions packaged as a loadable Claude Code skill. */
|
|
176
|
+
function generateDriverSkill(): string {
|
|
177
|
+
return `---
|
|
178
|
+
name: dz-harness-driver
|
|
179
|
+
description: >
|
|
180
|
+
Operating instructions for the dz-harness-hub CLI (@dzhechkov/harness-cli). Load this when
|
|
181
|
+
asked to install/manage AI skills, set up a project with dz, benchmark a skill, or pick
|
|
182
|
+
the right preset. Tells you which dz command to run and the rules for driving the toolkit.
|
|
183
|
+
Triggers on: "dz setup", "install skills", "benchmark skill", "which preset", "dz recommend".
|
|
184
|
+
trust_tier: 0
|
|
185
|
+
trust_tier_label: "Reference"
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
${generateDriverInstructions()}
|
|
189
|
+
`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Write the driver docs non-destructively. New files are created; an existing
|
|
194
|
+
* CLAUDE.md/AGENTS.md gets a marked section appended only if not already present.
|
|
195
|
+
* Returns a short detail string for the setup step.
|
|
196
|
+
*/
|
|
197
|
+
function installDriverDocs(projectRoot: string, force: boolean): string {
|
|
198
|
+
const written: string[] = [];
|
|
199
|
+
const skipped: string[] = [];
|
|
200
|
+
|
|
201
|
+
// 1. The loadable skill (own directory — always safe to write/refresh)
|
|
202
|
+
const skillDir = join(projectRoot, '.claude', 'skills', 'dz-harness-driver');
|
|
203
|
+
const skillPath = join(skillDir, 'SKILL.md');
|
|
204
|
+
if (!existsSync(skillPath) || force) {
|
|
205
|
+
mkdirSync(skillDir, { recursive: true });
|
|
206
|
+
writeFileSync(skillPath, generateDriverSkill());
|
|
207
|
+
written.push('skill');
|
|
208
|
+
} else {
|
|
209
|
+
skipped.push('skill');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 2. Standalone agent docs — create only if absent (never clobber the user's).
|
|
213
|
+
const instructions = generateDriverInstructions();
|
|
214
|
+
for (const name of ['AGENTS.md', 'GEMINI.md']) {
|
|
215
|
+
const p = join(projectRoot, name);
|
|
216
|
+
if (!existsSync(p)) {
|
|
217
|
+
writeFileSync(p, instructions);
|
|
218
|
+
written.push(name);
|
|
219
|
+
} else {
|
|
220
|
+
skipped.push(name);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 3. CLAUDE.md — append a marked block if the file lacks one (additive, idempotent).
|
|
225
|
+
const claudePath = join(projectRoot, 'CLAUDE.md');
|
|
226
|
+
const block = `\n${DRIVER_MARKER_START}\n\n${instructions}\n${DRIVER_MARKER_END}\n`;
|
|
227
|
+
if (!existsSync(claudePath)) {
|
|
228
|
+
writeFileSync(claudePath, block.trimStart());
|
|
229
|
+
written.push('CLAUDE.md');
|
|
230
|
+
} else {
|
|
231
|
+
const existing = readFileSync(claudePath, 'utf-8');
|
|
232
|
+
if (!existing.includes(DRIVER_MARKER_START)) {
|
|
233
|
+
writeFileSync(claudePath, existing + block);
|
|
234
|
+
written.push('CLAUDE.md(appended)');
|
|
235
|
+
} else {
|
|
236
|
+
skipped.push('CLAUDE.md');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const parts: string[] = [];
|
|
241
|
+
if (written.length) parts.push(`wrote ${written.join(', ')}`);
|
|
242
|
+
if (skipped.length) parts.push(`skipped ${skipped.join(', ')}`);
|
|
243
|
+
return parts.join('; ') || 'no changes';
|
|
244
|
+
}
|
|
245
|
+
|
|
125
246
|
export function runSetup(opts: SetupOptions): SetupResult {
|
|
126
247
|
const steps: SetupStep[] = [];
|
|
127
248
|
const dzDir = join(opts.projectRoot, '.dz');
|
|
@@ -268,6 +389,12 @@ export function runSetup(opts: SetupOptions): SetupResult {
|
|
|
268
389
|
steps.push({ name: 'Create .gitignore', status: 'done', detail: 'with .dz entries' });
|
|
269
390
|
}
|
|
270
391
|
|
|
392
|
+
// Step 7: Install the CLI-driver skill + agent docs (--install-driver)
|
|
393
|
+
if (opts.installDriver) {
|
|
394
|
+
const detail = installDriverDocs(opts.projectRoot, opts.force ?? false);
|
|
395
|
+
steps.push({ name: 'Install driver skill', status: 'done', detail });
|
|
396
|
+
}
|
|
397
|
+
|
|
271
398
|
return {
|
|
272
399
|
steps,
|
|
273
400
|
totalSteps: steps.length,
|
package/src/sync-upstream.ts
CHANGED
|
@@ -57,7 +57,11 @@ export function loadSourcesManifest(packageDir: string): SourcesManifest | undef
|
|
|
57
57
|
const sourcesPath = join(packageDir, 'sources.json');
|
|
58
58
|
if (!existsSync(sourcesPath)) return undefined;
|
|
59
59
|
const raw = readFileSync(sourcesPath, 'utf-8');
|
|
60
|
-
|
|
60
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
61
|
+
// Only return if it has the expected origin + skills structure
|
|
62
|
+
if (!parsed.origin || typeof parsed.origin !== 'object') return undefined;
|
|
63
|
+
if (!parsed.skills || typeof parsed.skills !== 'object') return undefined;
|
|
64
|
+
return parsed as unknown as SourcesManifest;
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
/** Info about a package with external sources. */
|