@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/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', 'build'],
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', 'pr', 'pull request', 'code review', 'merge'],
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
- export function recommend(task: string, registry: Registry): RecommendationReport {
172
- const topics = extractTopics(task);
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,
@@ -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
- return JSON.parse(raw) as SourcesManifest;
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. */