all-hands-cli 0.1.6 → 0.1.8

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 (46) hide show
  1. package/.allhands/flows/COMPOUNDING.md +3 -3
  2. package/.allhands/flows/EMERGENT_PLANNING.md +1 -1
  3. package/.allhands/flows/INITIATIVE_STEERING.md +0 -1
  4. package/.allhands/flows/PROMPT_TASK_EXECUTION.md +0 -1
  5. package/.allhands/flows/SPEC_PLANNING.md +1 -2
  6. package/.allhands/flows/harness/WRITING_HARNESS_FLOWS.md +1 -1
  7. package/.allhands/flows/harness/WRITING_HARNESS_KNOWLEDGE.md +1 -1
  8. package/.allhands/flows/harness/WRITING_HARNESS_ORCHESTRATION.md +1 -1
  9. package/.allhands/flows/harness/WRITING_HARNESS_SKILLS.md +1 -1
  10. package/.allhands/flows/harness/WRITING_HARNESS_TOOLS.md +1 -1
  11. package/.allhands/flows/harness/WRITING_HARNESS_VALIDATION_TOOLING.md +1 -1
  12. package/.allhands/flows/shared/CODEBASE_UNDERSTANDING.md +2 -3
  13. package/.allhands/flows/shared/CREATE_VALIDATION_TOOLING_SPEC.md +1 -1
  14. package/.allhands/flows/shared/PLAN_DEEPENING.md +2 -3
  15. package/.allhands/flows/shared/PROMPT_TASKS_CURATION.md +3 -5
  16. package/.allhands/flows/shared/WRITING_HARNESS_FLOWS.md +1 -1
  17. package/.allhands/flows/shared/jury/BEST_PRACTICES_REVIEW.md +4 -4
  18. package/.allhands/harness/src/cli.ts +4 -0
  19. package/.allhands/harness/src/commands/knowledge.ts +8 -5
  20. package/.allhands/harness/src/commands/skills.ts +299 -16
  21. package/.allhands/harness/src/commands/solutions.ts +227 -111
  22. package/.allhands/harness/src/commands/spawn.ts +6 -13
  23. package/.allhands/harness/src/hooks/shared.ts +1 -0
  24. package/.allhands/harness/src/lib/opencode/index.ts +65 -0
  25. package/.allhands/harness/src/lib/opencode/prompts/skills-aggregator.md +77 -0
  26. package/.allhands/harness/src/lib/opencode/prompts/solutions-aggregator.md +97 -0
  27. package/.allhands/harness/src/lib/opencode/runner.ts +98 -5
  28. package/.allhands/settings.json +2 -1
  29. package/.allhands/skills/harness-maintenance/SKILL.md +1 -1
  30. package/.allhands/skills/harness-maintenance/references/harness-skills.md +1 -1
  31. package/.allhands/skills/harness-maintenance/references/knowledge-compounding.md +5 -10
  32. package/.allhands/skills/harness-maintenance/references/writing-flows.md +1 -1
  33. package/CLAUDE.md +1 -1
  34. package/bin/sync-cli.js +18 -1
  35. package/docs/flows/compounding.md +2 -2
  36. package/docs/flows/plan-deepening-and-research.md +2 -2
  37. package/docs/flows/validation-and-skills-integration.md +14 -8
  38. package/docs/flows/wip/wip-flows.md +1 -1
  39. package/docs/harness/cli/search-commands.md +9 -19
  40. package/docs/memories.md +1 -1
  41. package/package.json +1 -1
  42. package/specs/workflow-domain-configuration.spec.md +2 -2
  43. package/src/commands/push.ts +21 -2
  44. package/src/lib/git.ts +19 -0
  45. package/.allhands/flows/shared/SKILL_EXTRACTION.md +0 -84
  46. package/.allhands/harness/src/commands/memories.ts +0 -302
@@ -67,8 +67,8 @@ Identify patterns that indicate harness improvement opportunities:
67
67
  ## Memory Extraction
68
68
 
69
69
  Per **Knowledge Compounding**, capture learnings as memories:
70
- - Run `ah memories search <relevant terms>` to check for existing similar memories before writing duplicates
71
- - Write to `docs/memories.md` (searchable via `ah memories search`)
70
+ - Run `ah solutions search "<relevant terms>"` to check for existing similar memories before writing duplicates
71
+ - Write to `docs/memories.md`
72
72
  - Format: `[Name] | [Domain] | [Source] | [Description]`
73
73
  - Domains: `planning`, `validation`, `implementation`, `harness-tooling`, `ideation`
74
74
  - Sources: `user-steering`, `agent-inferred`
@@ -113,7 +113,7 @@ For each documentable solution:
113
113
  ### Cross-Reference Solutions
114
114
 
115
115
  After all solutions are written, cross-reference related solutions:
116
- - Run `ah solutions list` then `ah solutions search` with terms from each new solution
116
+ - Run `ah solutions search` with terms from each new solution to find related solutions
117
117
  - For solutions sharing components, tags, or thematic overlap: add "## Related" section with links
118
118
  - Update existing similar solutions with cross-reference back to new solutions
119
119
 
@@ -17,7 +17,7 @@ Plan hypotheses as prompt files for executors to implement. Per **Quality Engine
17
17
  - Read `core_consolidation` from alignment doc frontmatter (default: `pending` if missing)
18
18
  - Read the workflow domain config at `WORKFLOW_DOMAIN_PATH` for `max_tangential_hypotheses`
19
19
  - Identify gaps between current state (completed work) and desired state (spec goals + success criteria)
20
- - Run `ah memories search "<hypothesis terms>"` for relevant prior insights
20
+ - Run `ah solutions search "<hypothesis terms>"` for relevant prior insights
21
21
 
22
22
  ## Phase Determination
23
23
 
@@ -34,7 +34,6 @@ Ground against current execution state — this is the core difference from spec
34
34
  - Compare completed work (prompt summaries) against spec goals
35
35
  - Identify gaps, risks, and drift between plan and reality
36
36
  - Run `ah solutions search "<steering context keywords>"` for relevant past solutions
37
- - Run `ah memories search "<steering context keywords>"` for relevant learnings
38
37
 
39
38
  ## Deep Grounding
40
39
 
@@ -18,7 +18,6 @@ Execute prompt tasks with full context, validate thoroughly, and document your w
18
18
  - Only if additional context is needed (likely not needed):
19
19
  - Run `ah knowledge docs search <descriptive_query>` for codebase information as needed
20
20
  - Run `ah solutions search "<keywords>"` for relevant past solutions
21
- - Run `ah memories search "<keywords>"` for relevant learnings and engineer preferences
22
21
 
23
22
  ## Implementation
24
23
 
@@ -31,8 +31,7 @@ Transform the spec into executable prompts with domain-appropriate planning dept
31
31
  - Read the alignment doc for existing prompts that may impact planning (if exists)
32
32
  - Read codebase files referenced in spec for initial grounding
33
33
  - Ensure your branch is up to date with base branch
34
- - Search documented solutions with `ah solutions search "<keywords>"` for relevant past learnings
35
- - Search memories with `ah memories search "<keywords>"` for engineer preferences and prior spec insights
34
+ - Run `ah solutions search "<keywords>"` for relevant past learnings and engineer preferences
36
35
 
37
36
  ## Idempotency Check
38
37
 
@@ -20,7 +20,7 @@ Guide agents through flow authoring with harness conventions. Per **Context is P
20
20
  ## Execution
21
21
 
22
22
  - Read `.allhands/principles.md` for first principle context
23
- - Run `ah skills list` to discover the `harness-maintenance` skill
23
+ - Run `ah skills search` to discover the `harness-maintenance` skill
24
24
  - Read the skill's `references/writing-flows.md` for flow authoring patterns
25
25
  - Author the flow using conventions: `<goal>`, `<inputs>`, `<outputs>`, `<constraints>`, action-verb bullets
26
26
  - Verify flow follows progressive disclosure — reference sub-flows rather than inlining complexity
@@ -20,7 +20,7 @@ Guide agents through knowledge compounding infrastructure — docs, solutions, m
20
20
  ## Execution
21
21
 
22
22
  - Read `.allhands/principles.md` for first principle context
23
- - Run `ah skills list` to discover the `harness-maintenance` skill
23
+ - Run `ah skills search` to discover the `harness-maintenance` skill
24
24
  - Read the skill's `references/knowledge-compounding.md` for schemas and compounding patterns
25
25
  - Create or update the knowledge artifact following type-specific conventions
26
26
  - Ensure proper indexing for discoverability via `ah knowledge docs search` or `ah solutions search`
@@ -20,7 +20,7 @@ Guide agents through orchestration layer changes — TUI lifecycle, event loop,
20
20
  ## Execution
21
21
 
22
22
  - Read `.allhands/principles.md` for first principle context
23
- - Run `ah skills list` to discover the `harness-maintenance` skill
23
+ - Run `ah skills search` to discover the `harness-maintenance` skill
24
24
  - Read the skill's `references/core-architecture.md` for architecture, schemas, and lifecycle patterns
25
25
  - Implement changes preserving architectural invariants (graceful degradation, semantic validation, in-memory state)
26
26
  - Validate with `ah validate agents` after profile modifications
@@ -20,7 +20,7 @@ Guide agents through skill creation and maintenance. Per **Context is Precious**
20
20
  ## Execution
21
21
 
22
22
  - Read `.allhands/principles.md` for first principle context
23
- - Run `ah skills list` to discover the `harness-maintenance` skill
23
+ - Run `ah skills search` to discover the `harness-maintenance` skill
24
24
  - Read the skill's `references/harness-skills.md` for skill schema, discovery mechanism, and conventions
25
25
  - Create or update the skill following hub-and-spoke pattern
26
26
  - Ensure glob coverage matches the skill's domain files
@@ -20,7 +20,7 @@ Guide agents through adding or modifying harness tools (CLI commands, hooks, MCP
20
20
  ## Execution
21
21
 
22
22
  - Read `.allhands/principles.md` for first principle context
23
- - Run `ah skills list` to discover the `harness-maintenance` skill
23
+ - Run `ah skills search` to discover the `harness-maintenance` skill
24
24
  - Read the skill's `references/tools-commands-mcp-hooks.md` for tool architecture and patterns
25
25
  - Implement the tool following auto-discovery conventions
26
26
  - Validate with `ah validate agents` if agent profiles are affected
@@ -20,7 +20,7 @@ Guide agents through validation suite creation. Per **Agentic Validation Tooling
20
20
  ## Execution
21
21
 
22
22
  - Read `.allhands/principles.md` for first principle context
23
- - Run `ah skills list` to discover the `harness-maintenance` skill
23
+ - Run `ah skills search` to discover the `harness-maintenance` skill
24
24
  - Read the skill's `references/validation-tooling.md` for suite philosophy and crystallization patterns
25
25
  - Design the suite with both stochastic and deterministic sections
26
26
  - Validate suite existence threshold — ensure meaningful stochastic dimension exists
@@ -19,8 +19,7 @@ Choose the right tool for the query type:
19
19
  | **Great codebase navigation tool AND documented knowledge!!** | `ah knowledge docs search` | "How does X work?", "Why is Y designed this way?" |
20
20
  | **Find relevant codebase patterns when knowledge search is not enough** | `tldr semantic search` or grep | Known string, error message, literal pattern |
21
21
  | **Find symbol definition - usually from symbols given by knowledge search** | LSP | Class, function, type by name |
22
- | **Past solutions** | `ah solutions search` | Similar problem solved before |
23
- | **Past learnings** | `ah memories search` | Engineer preferences, validation gaps, prior insights |
22
+ | **Past solutions + learnings** | `ah solutions search` | Similar problem solved before, engineer preferences, prior insights |
24
23
  | **Grep but better** | `ast-grep` | Known string, error message, literal pattern |
25
24
 
26
25
  ### Search Flow
@@ -58,7 +57,7 @@ Need codebase context?
58
57
  └─ Direct result? → relevant_files + [ref:...] blocks → LSP on symbols
59
58
  ├─ Know exact symbol? → LSP directly
60
59
  ├─ Know semantic idea? → tldr semantic search / grep
61
- ├─ Suspect a similar problem faced before? → ah solutions search + ah memories search first
60
+ ├─ Suspect a similar problem faced before? → ah solutions search first
62
61
  └─ ast-grep if still struggling
63
62
  ```
64
63
 
@@ -21,7 +21,7 @@ Create a validation tooling spec for a new domain. Per **Prompt Files as Units o
21
21
 
22
22
  ## Domain Knowledge
23
23
 
24
- - Run `ah skills list` to discover the `harness-maintenance` skill
24
+ - Run `ah skills search` to discover the `harness-maintenance` skill
25
25
  - Read the skill's `references/validation-tooling.md` for suite writing philosophy, crystallization lifecycle, evidence capture patterns, and tool validation guidance
26
26
 
27
27
  ## Research
@@ -36,8 +36,8 @@ Spawn parallel subtasks for each research area:
36
36
  ### Skill Application
37
37
 
38
38
  Per **Frontier Models are Capable**, match skills to plan content:
39
- - Run `ah skills list` to discover available skills
40
- - For each domain in the plan, spawn subtask:
39
+ - For each domain in the plan, run `ah skills search` to find applicable skills
40
+ - For matched skills, spawn subtask:
41
41
  - Read matched skill's SKILL.md
42
42
  - Apply skill patterns to relevant prompts
43
43
  - Return best practices and gotchas
@@ -46,7 +46,6 @@ Per **Frontier Models are Capable**, match skills to plan content:
46
46
 
47
47
  Per **Knowledge Compounding**, check for relevant past solutions:
48
48
  - Run `ah solutions search "<domain keywords>"` for each technology area
49
- - Run `ah memories search "<domain keywords>"` for relevant learnings and engineer preferences
50
49
  - For high-scoring matches, extract:
51
50
  - Key insights that apply
52
51
  - Gotchas to avoid
@@ -44,12 +44,10 @@ Create, edit, and maintain Prompt Task files - the atomic unit of work. Per **Pr
44
44
 
45
45
  Skills embed domain expertise into prompts - "how to do it right."
46
46
 
47
- Read `.allhands/flows/shared/SKILL_EXTRACTION.md` and:
48
- - Run `ah skills list` to discover available skills
49
- - Match skills to the prompt's domain (by globs and description)
50
- - Read matched skill files for patterns, best practices, guidelines
51
- - Extract relevant knowledge and embed in Tasks section
47
+ Run `ah skills search` with the prompt's domain and files being touched:
48
+ - Embed returned skill guidance in the prompt's Tasks section
52
49
  - Add matched skill file paths to `skills` frontmatter
50
+ - Read skill reference files if deeper detail is needed
53
51
 
54
52
  Skills provide: code patterns, library preferences, common pitfalls, domain-specific best practices.
55
53
 
@@ -4,7 +4,7 @@ Flow authoring conventions for the All Hands harness. Per **Knowledge Compoundin
4
4
 
5
5
  ## Start Here
6
6
 
7
- - Run `ah skills list` to discover the `harness-maintenance` skill
7
+ - Run `ah skills search` to discover the `harness-maintenance` skill
8
8
  - Read the skill's routing table — select `references/writing-flows.md` for flow authoring patterns
9
9
  - For execution, follow `.allhands/flows/harness/WRITING_HARNESS_FLOWS.md`
10
10
 
@@ -15,7 +15,7 @@ Review implementation for domain best practices compliance. Per **Knowledge Comp
15
15
  </outputs>
16
16
 
17
17
  <constraints>
18
- - MUST extract skills using SKILL_EXTRACTION.md subtask
18
+ - MUST extract skills using `ah skills search`
19
19
  - MUST search codebase knowledge for established patterns
20
20
  - MUST use research tools if no skill findings exist for the domain
21
21
  - MUST order issues by priority for fixing
@@ -29,9 +29,9 @@ Review implementation for domain best practices compliance. Per **Knowledge Comp
29
29
 
30
30
  ## Best Practices Extraction
31
31
 
32
- Spawn subtask to read `.allhands/flows/shared/SKILL_EXTRACTION.md`:
33
- - Provide the domain files as input
34
- - Extract patterns, preferences, and pitfalls for this domain
32
+ Run `ah skills search` with the domain and relevant files:
33
+ - Use returned guidance and skill references as review criteria
34
+ - Read skill reference files if deeper detail is needed
35
35
 
36
36
  Search codebase knowledge:
37
37
  - Run `ah knowledge docs search "<domain> best practices"` for established patterns
@@ -28,6 +28,10 @@ async function main(): Promise<void> {
28
28
  await discoverAndRegister(program);
29
29
 
30
30
  await program.parseAsync();
31
+
32
+ // CLI subcommands may leave open handles (e.g. OpenCode SDK server sockets).
33
+ // TUI manages its own process.exit(), so this only affects subcommand runs.
34
+ process.exit(0);
31
35
  }
32
36
 
33
37
  main().catch((e) => {
@@ -17,6 +17,7 @@ import { dirname, join } from "path";
17
17
  import { fileURLToPath } from "url";
18
18
  import {
19
19
  AgentRunner,
20
+ withDebugInfo,
20
21
  type AggregatorOutput,
21
22
  type SearchResult,
22
23
  } from "../lib/opencode/index.js";
@@ -121,13 +122,15 @@ class SearchCommand extends BaseCommand {
121
122
  cmd
122
123
  .argument("<query>", "Descriptive phrase (e.g. 'how to handle API authentication')")
123
124
  .option("--metadata-only", "Return only file paths and descriptions (no full content)")
124
- .option("--no-aggregate", "Disable aggregation entirely");
125
+ .option("--no-aggregate", "Disable aggregation entirely")
126
+ .option("--debug", "Include agent debug metadata (model, timing, fallback) in output");
125
127
  }
126
128
 
127
129
  async execute(args: Record<string, unknown>): Promise<CommandResult> {
128
130
  const query = args.query as string;
129
131
  const metadataOnly = !!args.metadataOnly;
130
132
  const noAggregate = !!args.noAggregate;
133
+ const debug = !!args.debug;
131
134
 
132
135
  if (!query) {
133
136
  return this.error("validation_error", "query is required");
@@ -199,17 +202,17 @@ class SearchCommand extends BaseCommand {
199
202
 
200
203
  if (!agentResult.success) {
201
204
  // Fall back to raw results on aggregation failure
202
- return this.success({
205
+ return this.success(withDebugInfo({
203
206
  index: this.indexName,
204
207
  query,
205
208
  results,
206
209
  result_count: results.length,
207
210
  aggregated: false,
208
211
  aggregation_error: agentResult.error,
209
- });
212
+ }, agentResult, debug));
210
213
  }
211
214
 
212
- return this.success({
215
+ return this.success(withDebugInfo({
213
216
  index: this.indexName,
214
217
  query,
215
218
  aggregated: true,
@@ -217,7 +220,7 @@ class SearchCommand extends BaseCommand {
217
220
  lsp_entry_points: agentResult.data!.lsp_entry_points,
218
221
  design_notes: agentResult.data!.design_notes,
219
222
  source_results: results.length,
220
- });
223
+ }, agentResult, debug));
221
224
  } catch (error) {
222
225
  const message = error instanceof Error ? error.message : String(error);
223
226
  return this.error("search_error", message);
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Skills Command (Agent-Facing)
3
3
  *
4
- * Lists and discovers skills for domain expertise.
4
+ * Searches and discovers skills for domain expertise.
5
5
  * Agents use this to find relevant skills for their tasks.
6
6
  *
7
- * Usage: ah skills list
7
+ * Usage:
8
+ * ah skills search <query> [--paths <paths...>] [--limit <n>] [--no-aggregate]
8
9
  */
9
10
 
10
11
  import { Command } from 'commander';
@@ -12,7 +13,9 @@ import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
12
13
  import { join, dirname } from 'path';
13
14
  import { fileURLToPath } from 'url';
14
15
  import { parse as parseYaml } from 'yaml';
16
+ import { minimatch } from 'minimatch';
15
17
  import { tracedAction } from '../lib/base-command.js';
18
+ import { AgentRunner, withDebugInfo, type SkillSearchOutput } from '../lib/opencode/index.js';
16
19
 
17
20
  const __filename = fileURLToPath(import.meta.url);
18
21
  const __dirname = dirname(__filename);
@@ -32,6 +35,31 @@ interface SkillEntry {
32
35
  file: string;
33
36
  }
34
37
 
38
+ interface SkillMatch extends SkillEntry {
39
+ score: number;
40
+ matchedFields: string[];
41
+ pathMatch: boolean;
42
+ }
43
+
44
+ /** Scoring weights for keyword matching against skill fields. */
45
+ const SCORE_WEIGHT = {
46
+ NAME: 3,
47
+ DESCRIPTION: 3,
48
+ GLOBS: 2,
49
+ PATH_BOOST: 4,
50
+ } as const;
51
+
52
+ // Load aggregator prompt from file
53
+ const AGGREGATOR_PROMPT_PATH = join(__dirname, '../lib/opencode/prompts/skills-aggregator.md');
54
+
55
+ const getAggregatorPrompt = (): string => {
56
+ return readFileSync(AGGREGATOR_PROMPT_PATH, 'utf-8');
57
+ };
58
+
59
+ const getProjectRoot = (): string => {
60
+ return process.env.PROJECT_ROOT || process.cwd();
61
+ };
62
+
35
63
  /**
36
64
  * Extract frontmatter from markdown content
37
65
  */
@@ -50,6 +78,14 @@ function extractFrontmatter(content: string): Record<string, unknown> | null {
50
78
  }
51
79
  }
52
80
 
81
+ /**
82
+ * Extract body content from markdown (everything after frontmatter)
83
+ */
84
+ function extractBody(content: string): string {
85
+ const frontmatterRegex = /^---\n[\s\S]*?\n---\n?/;
86
+ return content.replace(frontmatterRegex, '').trim();
87
+ }
88
+
53
89
  /**
54
90
  * Get the skills directory path
55
91
  * Path: harness/src/commands/ -> harness/src/ -> harness/ -> .allhands/ -> skills/
@@ -98,31 +134,278 @@ function listSkills(): SkillEntry[] {
98
134
  return skills;
99
135
  }
100
136
 
137
+ /**
138
+ * Extract keywords from a search query.
139
+ * Handles quoted phrases and splits remaining words.
140
+ */
141
+ function extractKeywords(query: string): string[] {
142
+ const keywords: string[] = [];
143
+ const quotedRegex = /"([^"]+)"/g;
144
+ let remaining = query;
145
+ let match;
146
+
147
+ while ((match = quotedRegex.exec(query)) !== null) {
148
+ keywords.push(match[1].toLowerCase());
149
+ remaining = remaining.replace(match[0], '');
150
+ }
151
+
152
+ const words = remaining
153
+ .split(/\s+/)
154
+ .map(w => w.toLowerCase().trim())
155
+ .filter(w => w.length > 1);
156
+
157
+ keywords.push(...words);
158
+ return keywords;
159
+ }
160
+
161
+ /**
162
+ * Score a skill against search keywords.
163
+ * Returns score and which fields matched.
164
+ */
165
+ function scoreSkill(
166
+ entry: SkillEntry,
167
+ keywords: string[],
168
+ ): { score: number; matchedFields: string[] } {
169
+ let score = 0;
170
+ const matchedFields: string[] = [];
171
+ const nameLC = entry.name.toLowerCase();
172
+ const descLC = entry.description.toLowerCase();
173
+ const globsLC = entry.globs.join(' ').toLowerCase();
174
+
175
+ for (const kw of keywords) {
176
+ if (nameLC.includes(kw)) {
177
+ score += SCORE_WEIGHT.NAME;
178
+ if (!matchedFields.includes('name')) matchedFields.push('name');
179
+ }
180
+ if (descLC.includes(kw)) {
181
+ score += SCORE_WEIGHT.DESCRIPTION;
182
+ if (!matchedFields.includes('description')) matchedFields.push('description');
183
+ }
184
+ if (globsLC.includes(kw)) {
185
+ score += SCORE_WEIGHT.GLOBS;
186
+ if (!matchedFields.includes('globs')) matchedFields.push('globs');
187
+ }
188
+ }
189
+
190
+ return { score, matchedFields };
191
+ }
192
+
193
+ /**
194
+ * Check if a skill's globs match any of the provided file paths.
195
+ */
196
+ function matchesPaths(skill: SkillEntry, paths: string[]): boolean {
197
+ for (const filePath of paths) {
198
+ for (const glob of skill.globs) {
199
+ if (minimatch(filePath, glob)) {
200
+ return true;
201
+ }
202
+ }
203
+ }
204
+ return false;
205
+ }
206
+
207
+ /**
208
+ * Search skills by keyword scoring with optional path boosting.
209
+ */
210
+ function searchSkills(
211
+ query: string,
212
+ options: { paths?: string[]; limit?: number },
213
+ ): SkillMatch[] {
214
+ const { paths, limit = 10 } = options;
215
+ const allSkills = listSkills();
216
+ const keywords = extractKeywords(query);
217
+ const results: SkillMatch[] = [];
218
+
219
+ for (const skill of allSkills) {
220
+ const { score: keywordScore, matchedFields } = scoreSkill(skill, keywords);
221
+ const pathMatch = paths ? matchesPaths(skill, paths) : false;
222
+
223
+ let finalScore = keywordScore;
224
+
225
+ if (paths && pathMatch) {
226
+ finalScore = keywordScore > 0 ? keywordScore + SCORE_WEIGHT.PATH_BOOST : SCORE_WEIGHT.PATH_BOOST;
227
+ }
228
+
229
+ if (finalScore > 0) {
230
+ results.push({
231
+ ...skill,
232
+ score: finalScore,
233
+ matchedFields: pathMatch && matchedFields.length === 0
234
+ ? ['paths']
235
+ : pathMatch
236
+ ? [...matchedFields, 'paths']
237
+ : matchedFields,
238
+ pathMatch,
239
+ });
240
+ }
241
+ }
242
+
243
+ results.sort((a, b) => b.score - a.score);
244
+ return results.slice(0, limit);
245
+ }
246
+
247
+ /**
248
+ * Get the body content of a SKILL.md file (without frontmatter).
249
+ */
250
+ function getSkillContent(entry: SkillEntry): string | null {
251
+ const dir = getSkillsDir();
252
+ const skillFile = join(dir, entry.name, 'SKILL.md');
253
+
254
+ if (!existsSync(skillFile)) return null;
255
+
256
+ const content = readFileSync(skillFile, 'utf-8');
257
+ return extractBody(content);
258
+ }
259
+
260
+ /**
261
+ * Get reference file paths for a skill (from references/ and docs/ subdirs).
262
+ */
263
+ function getSkillReferenceFiles(entry: SkillEntry): string[] {
264
+ const dir = getSkillsDir();
265
+ const skillDir = join(dir, entry.name);
266
+ const refPaths: string[] = [];
267
+ const subdirs = ['references', 'docs'];
268
+
269
+ for (const subdir of subdirs) {
270
+ const subdirPath = join(skillDir, subdir);
271
+ if (!existsSync(subdirPath) || !statSync(subdirPath).isDirectory()) continue;
272
+
273
+ const files = readdirSync(subdirPath);
274
+ for (const file of files) {
275
+ if (file.endsWith('.md')) {
276
+ refPaths.push(`.allhands/skills/${entry.name}/${subdir}/${file}`);
277
+ }
278
+ }
279
+ }
280
+
281
+ return refPaths;
282
+ }
283
+
284
+ /**
285
+ * Run AI aggregation on skill matches to produce synthesized guidance.
286
+ * Returns a JSON-serializable result object.
287
+ */
288
+ async function aggregateSkills(
289
+ query: string,
290
+ matches: SkillMatch[],
291
+ debug: boolean,
292
+ ): Promise<Record<string, unknown>> {
293
+ const skillsInput = matches.map(m => {
294
+ const content = getSkillContent(m);
295
+ const referenceFiles = getSkillReferenceFiles(m);
296
+ return {
297
+ name: m.name,
298
+ description: m.description,
299
+ globs: m.globs,
300
+ file: m.file,
301
+ content: content || '',
302
+ reference_files: referenceFiles,
303
+ };
304
+ });
305
+
306
+ const userMessage = JSON.stringify({ query, skills: skillsInput });
307
+ const projectRoot = getProjectRoot();
308
+ const runner = new AgentRunner(projectRoot);
309
+
310
+ try {
311
+ const agentResult = await runner.run<SkillSearchOutput>(
312
+ {
313
+ name: 'skills-aggregator',
314
+ systemPrompt: getAggregatorPrompt(),
315
+ timeoutMs: 60000,
316
+ steps: 5,
317
+ },
318
+ userMessage,
319
+ );
320
+
321
+ if (!agentResult.success) {
322
+ return withDebugInfo({
323
+ success: true,
324
+ query,
325
+ matches,
326
+ count: matches.length,
327
+ aggregated: false,
328
+ aggregation_error: agentResult.error,
329
+ }, agentResult, debug);
330
+ }
331
+
332
+ return withDebugInfo({
333
+ success: true,
334
+ query,
335
+ aggregated: true,
336
+ guidance: agentResult.data!.guidance,
337
+ relevant_skills: agentResult.data!.relevant_skills,
338
+ design_notes: agentResult.data!.design_notes,
339
+ source_matches: matches.length,
340
+ }, agentResult, debug);
341
+ } catch (error) {
342
+ const message = error instanceof Error ? error.message : String(error);
343
+ return {
344
+ success: true,
345
+ query,
346
+ matches,
347
+ count: matches.length,
348
+ aggregated: false,
349
+ aggregation_error: message,
350
+ };
351
+ }
352
+ }
353
+
101
354
  export function register(program: Command): void {
102
355
  const cmd = program
103
356
  .command('skills')
104
- .description('Discover and list skills for domain expertise');
357
+ .description('Search and discover skills for domain expertise');
105
358
 
359
+ // Search subcommand
106
360
  cmd
107
- .command('list')
108
- .description('List all skills with their descriptions and glob patterns')
109
- .option('--json', 'Output as JSON (default)')
110
- .action(tracedAction('skills list', async () => {
111
- const skills = listSkills();
361
+ .command('search')
362
+ .description('Search skills by query with optional path boosting and AI aggregation')
363
+ .argument('<query>', 'Descriptive search query (e.g. "how to write a flow")')
364
+ .option('--paths <paths...>', 'File paths to match against skill globs (boosts relevance)')
365
+ .option('--limit <n>', 'Maximum results', '10')
366
+ .option('--no-aggregate', 'Skip aggregation, return raw matches')
367
+ .option('--debug', 'Include agent debug metadata (model, timing, fallback) in output')
368
+ .action(tracedAction('skills search', async (query: string, _opts: Record<string, unknown>, command: Command) => {
369
+ const opts = command.opts();
370
+ const paths = opts.paths as string[] | undefined;
371
+ const limit = parseInt(opts.limit as string, 10) || 10;
372
+ const noAggregate = opts.aggregate === false;
373
+ const debug = !!opts.debug;
374
+
375
+ if (!query) {
376
+ console.log(JSON.stringify({
377
+ success: false,
378
+ error: 'validation_error: query is required',
379
+ }, null, 2));
380
+ return;
381
+ }
112
382
 
113
- if (skills.length === 0) {
383
+ const matches = searchSkills(query, { paths, limit });
384
+
385
+ if (matches.length === 0) {
114
386
  console.log(JSON.stringify({
115
387
  success: true,
116
- skills: [],
117
- message: 'No skills found. Create skills in .allhands/skills/<name>/SKILL.md using `ah schema skill` for the file structure.',
388
+ query,
389
+ matches: [],
390
+ count: 0,
391
+ message: 'No skills matched the search query.',
118
392
  }, null, 2));
119
393
  return;
120
394
  }
121
395
 
122
- console.log(JSON.stringify({
123
- success: true,
124
- skills,
125
- count: skills.length,
126
- }, null, 2));
396
+ // Return raw matches if aggregation disabled
397
+ if (noAggregate) {
398
+ console.log(JSON.stringify({
399
+ success: true,
400
+ query,
401
+ matches,
402
+ count: matches.length,
403
+ aggregated: false,
404
+ }, null, 2));
405
+ return;
406
+ }
407
+
408
+ const result = await aggregateSkills(query, matches, debug);
409
+ console.log(JSON.stringify(result, null, 2));
127
410
  }));
128
411
  }