all-hands-cli 0.1.5 → 0.1.7
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/.allhands/flows/COMPOUNDING.md +3 -3
- package/.allhands/flows/EMERGENT_PLANNING.md +1 -1
- package/.allhands/flows/INITIATIVE_STEERING.md +0 -1
- package/.allhands/flows/PROMPT_TASK_EXECUTION.md +0 -1
- package/.allhands/flows/SPEC_PLANNING.md +1 -2
- package/.allhands/flows/harness/WRITING_HARNESS_FLOWS.md +1 -1
- package/.allhands/flows/harness/WRITING_HARNESS_KNOWLEDGE.md +1 -1
- package/.allhands/flows/harness/WRITING_HARNESS_ORCHESTRATION.md +1 -1
- package/.allhands/flows/harness/WRITING_HARNESS_SKILLS.md +1 -1
- package/.allhands/flows/harness/WRITING_HARNESS_TOOLS.md +1 -1
- package/.allhands/flows/harness/WRITING_HARNESS_VALIDATION_TOOLING.md +1 -1
- package/.allhands/flows/shared/CODEBASE_UNDERSTANDING.md +2 -3
- package/.allhands/flows/shared/CREATE_VALIDATION_TOOLING_SPEC.md +1 -1
- package/.allhands/flows/shared/PLAN_DEEPENING.md +2 -3
- package/.allhands/flows/shared/PROMPT_TASKS_CURATION.md +3 -5
- package/.allhands/flows/shared/WRITING_HARNESS_FLOWS.md +1 -1
- package/.allhands/flows/shared/jury/BEST_PRACTICES_REVIEW.md +4 -4
- package/.allhands/harness/src/cli.ts +4 -0
- package/.allhands/harness/src/commands/knowledge.ts +8 -5
- package/.allhands/harness/src/commands/skills.ts +299 -16
- package/.allhands/harness/src/commands/solutions.ts +227 -111
- package/.allhands/harness/src/commands/spawn.ts +6 -13
- package/.allhands/harness/src/hooks/shared.ts +1 -0
- package/.allhands/harness/src/lib/opencode/index.ts +65 -0
- package/.allhands/harness/src/lib/opencode/prompts/skills-aggregator.md +77 -0
- package/.allhands/harness/src/lib/opencode/prompts/solutions-aggregator.md +97 -0
- package/.allhands/harness/src/lib/opencode/runner.ts +98 -5
- package/.allhands/settings.json +2 -1
- package/.allhands/skills/harness-maintenance/SKILL.md +1 -1
- package/.allhands/skills/harness-maintenance/references/harness-skills.md +1 -1
- package/.allhands/skills/harness-maintenance/references/knowledge-compounding.md +5 -10
- package/.allhands/skills/harness-maintenance/references/validation-tooling.md +10 -1
- package/.allhands/skills/harness-maintenance/references/writing-flows.md +1 -1
- package/CLAUDE.md +1 -1
- package/docs/flows/compounding.md +2 -2
- package/docs/flows/plan-deepening-and-research.md +2 -2
- package/docs/flows/validation-and-skills-integration.md +14 -8
- package/docs/flows/wip/wip-flows.md +1 -1
- package/docs/harness/cli/search-commands.md +9 -19
- package/docs/memories.md +1 -1
- package/package.json +1 -1
- package/specs/workflow-domain-configuration.spec.md +2 -2
- package/.allhands/flows/shared/SKILL_EXTRACTION.md +0 -84
- 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
|
|
71
|
-
- Write to `docs/memories.md`
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
40
|
-
- For
|
|
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
|
-
|
|
48
|
-
-
|
|
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
|
|
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
|
|
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
|
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
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
|
-
*
|
|
4
|
+
* Searches and discovers skills for domain expertise.
|
|
5
5
|
* Agents use this to find relevant skills for their tasks.
|
|
6
6
|
*
|
|
7
|
-
* Usage:
|
|
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('
|
|
357
|
+
.description('Search and discover skills for domain expertise');
|
|
105
358
|
|
|
359
|
+
// Search subcommand
|
|
106
360
|
cmd
|
|
107
|
-
.command('
|
|
108
|
-
.description('
|
|
109
|
-
.
|
|
110
|
-
.
|
|
111
|
-
|
|
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
|
-
|
|
383
|
+
const matches = searchSkills(query, { paths, limit });
|
|
384
|
+
|
|
385
|
+
if (matches.length === 0) {
|
|
114
386
|
console.log(JSON.stringify({
|
|
115
387
|
success: true,
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
}
|