all-hands-cli 0.1.0
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/README.md +75 -0
- package/.allhands/agents/compounder.yaml +15 -0
- package/.allhands/agents/coordinator.yaml +17 -0
- package/.allhands/agents/documentor.yaml +15 -0
- package/.allhands/agents/e2e-test-planner.yaml +17 -0
- package/.allhands/agents/emergent.yaml +22 -0
- package/.allhands/agents/executor.yaml +14 -0
- package/.allhands/agents/ideation.yaml +11 -0
- package/.allhands/agents/initiative-steering.yaml +19 -0
- package/.allhands/agents/judge.yaml +13 -0
- package/.allhands/agents/planner.yaml +19 -0
- package/.allhands/agents/pr-reviewer.yaml +15 -0
- package/.allhands/docs.json +5 -0
- package/.allhands/docs.local.json +26 -0
- package/.allhands/flows/COMPOUNDING.md +203 -0
- package/.allhands/flows/COORDINATION.md +89 -0
- package/.allhands/flows/CORE.md +87 -0
- package/.allhands/flows/DOCUMENTATION.md +218 -0
- package/.allhands/flows/E2E_TEST_PLAN_BUILDING.md +140 -0
- package/.allhands/flows/EMERGENT_PLANNING.md +57 -0
- package/.allhands/flows/IDEATION_SCOPING.md +154 -0
- package/.allhands/flows/INITIATIVE_STEERING.md +110 -0
- package/.allhands/flows/JUDGE_REVIEWING.md +79 -0
- package/.allhands/flows/PROMPT_TASK_EXECUTION.md +68 -0
- package/.allhands/flows/PR_REVIEWING.md +43 -0
- package/.allhands/flows/SPEC_PLANNING.md +216 -0
- package/.allhands/flows/harness/WRITING_HARNESS_FLOWS.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_KNOWLEDGE.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_ORCHESTRATION.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_SKILLS.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_TOOLS.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_VALIDATION_TOOLING.md +27 -0
- package/.allhands/flows/shared/CODEBASE_UNDERSTANDING.md +72 -0
- package/.allhands/flows/shared/CREATE_HARNESS_SPEC.md +48 -0
- package/.allhands/flows/shared/CREATE_SPEC.md +41 -0
- package/.allhands/flows/shared/CREATE_VALIDATION_TOOLING_SPEC.md +70 -0
- package/.allhands/flows/shared/DOCUMENTATION_DISCOVERY.md +123 -0
- package/.allhands/flows/shared/DOCUMENTATION_WRITER.md +101 -0
- package/.allhands/flows/shared/EMERGENT_REFINEMENT_ANALYSIS.md +76 -0
- package/.allhands/flows/shared/EXTERNAL_TECH_GUIDANCE.md +97 -0
- package/.allhands/flows/shared/IDEATION_CODEBASE_GROUNDING.md +49 -0
- package/.allhands/flows/shared/PLAN_DEEPENING.md +152 -0
- package/.allhands/flows/shared/PROMPT_TASKS_CURATION.md +113 -0
- package/.allhands/flows/shared/PROMPT_VALIDATION_REVIEW.MD +99 -0
- package/.allhands/flows/shared/QUICK_PREMORTEM.md +70 -0
- package/.allhands/flows/shared/RESEARCH_GUIDANCE.md +38 -0
- package/.allhands/flows/shared/REVIEW_OPTIONS_BREAKDOWN.md +68 -0
- package/.allhands/flows/shared/SKILL_EXTRACTION.md +84 -0
- package/.allhands/flows/shared/SPEC_FLOW_ANALYSIS.md +119 -0
- package/.allhands/flows/shared/TDD_WORKFLOW.md +109 -0
- package/.allhands/flows/shared/UTILIZE_VALIDATION_TOOLING.md +84 -0
- package/.allhands/flows/shared/WRITING_HARNESS_FLOWS.md +11 -0
- package/.allhands/flows/shared/WRITING_HARNESS_MCP_TOOLS.md +84 -0
- package/.allhands/flows/shared/jury/ARCHITECTURE_REVIEW.md +91 -0
- package/.allhands/flows/shared/jury/BEST_PRACTICES_REVIEW.md +80 -0
- package/.allhands/flows/shared/jury/CLAIM_VERIFICATION_REVIEW.md +101 -0
- package/.allhands/flows/shared/jury/EXPECTATIONS_FIT_REVIEW.md +78 -0
- package/.allhands/flows/shared/jury/MAINTAINABILITY_REVIEW.md +110 -0
- package/.allhands/flows/shared/jury/PROMPTS_EXPECTATIONS_FIT.md +74 -0
- package/.allhands/flows/shared/jury/PROMPTS_FLOW_ANALYSIS.md +92 -0
- package/.allhands/flows/shared/jury/PROMPTS_YAGNI.md +78 -0
- package/.allhands/flows/shared/jury/PROMPT_PREMORTEM.md +125 -0
- package/.allhands/flows/shared/jury/SECURITY_REVIEW.md +86 -0
- package/.allhands/flows/shared/jury/YAGNI_REVIEW.md +82 -0
- package/.allhands/flows/wip/DEBUG_INVESTIGATION.md +162 -0
- package/.allhands/flows/wip/MEMORY_RECALL.md +62 -0
- package/.allhands/harness/ah +131 -0
- package/.allhands/harness/package-lock.json +5292 -0
- package/.allhands/harness/package.json +52 -0
- package/.allhands/harness/src/__tests__/e2e/commands.test.ts +307 -0
- package/.allhands/harness/src/__tests__/e2e/event-loop.test.ts +539 -0
- package/.allhands/harness/src/__tests__/e2e/hooks.test.ts +427 -0
- package/.allhands/harness/src/__tests__/e2e/new-initiative-routing.test.ts +137 -0
- package/.allhands/harness/src/__tests__/e2e/run-e2e.ts +109 -0
- package/.allhands/harness/src/__tests__/e2e/specs-type.test.ts +210 -0
- package/.allhands/harness/src/__tests__/e2e/validation-hooks.test.ts +669 -0
- package/.allhands/harness/src/__tests__/e2e/validation-path-consistency.test.ts +354 -0
- package/.allhands/harness/src/__tests__/e2e/validation.test.ts +528 -0
- package/.allhands/harness/src/__tests__/harness/assertions.ts +318 -0
- package/.allhands/harness/src/__tests__/harness/cli-runner.ts +359 -0
- package/.allhands/harness/src/__tests__/harness/fixture.ts +384 -0
- package/.allhands/harness/src/__tests__/harness/hook-runner.ts +411 -0
- package/.allhands/harness/src/__tests__/harness/index.ts +122 -0
- package/.allhands/harness/src/cli.ts +36 -0
- package/.allhands/harness/src/commands/complexity.ts +177 -0
- package/.allhands/harness/src/commands/context7.ts +202 -0
- package/.allhands/harness/src/commands/docs.ts +557 -0
- package/.allhands/harness/src/commands/hooks.ts +24 -0
- package/.allhands/harness/src/commands/index.ts +51 -0
- package/.allhands/harness/src/commands/knowledge.ts +382 -0
- package/.allhands/harness/src/commands/memories.ts +302 -0
- package/.allhands/harness/src/commands/notify.ts +61 -0
- package/.allhands/harness/src/commands/oracle.ts +158 -0
- package/.allhands/harness/src/commands/perplexity.ts +220 -0
- package/.allhands/harness/src/commands/planning.ts +245 -0
- package/.allhands/harness/src/commands/schema.ts +73 -0
- package/.allhands/harness/src/commands/skills.ts +128 -0
- package/.allhands/harness/src/commands/solutions.ts +353 -0
- package/.allhands/harness/src/commands/spawn.ts +158 -0
- package/.allhands/harness/src/commands/specs.ts +532 -0
- package/.allhands/harness/src/commands/tavily.ts +226 -0
- package/.allhands/harness/src/commands/tools.ts +579 -0
- package/.allhands/harness/src/commands/trace.ts +327 -0
- package/.allhands/harness/src/commands/tui.ts +960 -0
- package/.allhands/harness/src/commands/validate.ts +143 -0
- package/.allhands/harness/src/commands/validation-tools.ts +108 -0
- package/.allhands/harness/src/hooks/context.ts +1442 -0
- package/.allhands/harness/src/hooks/enforcement.ts +170 -0
- package/.allhands/harness/src/hooks/index.ts +54 -0
- package/.allhands/harness/src/hooks/lifecycle.ts +229 -0
- package/.allhands/harness/src/hooks/notification.ts +104 -0
- package/.allhands/harness/src/hooks/observability.ts +551 -0
- package/.allhands/harness/src/hooks/session.ts +88 -0
- package/.allhands/harness/src/hooks/shared.ts +815 -0
- package/.allhands/harness/src/hooks/transcript-parser.ts +208 -0
- package/.allhands/harness/src/hooks/validation.ts +617 -0
- package/.allhands/harness/src/lib/__tests__/ctags.test.ts +244 -0
- package/.allhands/harness/src/lib/__tests__/docs-validation.test.ts +344 -0
- package/.allhands/harness/src/lib/__tests__/mcp-runtime.test.ts +190 -0
- package/.allhands/harness/src/lib/__tests__/schema.test.ts +861 -0
- package/.allhands/harness/src/lib/base-command.ts +198 -0
- package/.allhands/harness/src/lib/cli-daemon.ts +343 -0
- package/.allhands/harness/src/lib/compaction.ts +313 -0
- package/.allhands/harness/src/lib/ctags.ts +497 -0
- package/.allhands/harness/src/lib/docs-validation.ts +907 -0
- package/.allhands/harness/src/lib/event-loop.ts +662 -0
- package/.allhands/harness/src/lib/flows.ts +155 -0
- package/.allhands/harness/src/lib/git.ts +276 -0
- package/.allhands/harness/src/lib/knowledge-worker.ts +72 -0
- package/.allhands/harness/src/lib/knowledge.ts +810 -0
- package/.allhands/harness/src/lib/llm.ts +255 -0
- package/.allhands/harness/src/lib/mcp-client.ts +432 -0
- package/.allhands/harness/src/lib/mcp-daemon.ts +486 -0
- package/.allhands/harness/src/lib/mcp-runtime.ts +418 -0
- package/.allhands/harness/src/lib/notification.ts +115 -0
- package/.allhands/harness/src/lib/opencode/index.ts +70 -0
- package/.allhands/harness/src/lib/opencode/profiles.ts +300 -0
- package/.allhands/harness/src/lib/opencode/prompts/codesearch.md +98 -0
- package/.allhands/harness/src/lib/opencode/prompts/knowledge-aggregator.md +67 -0
- package/.allhands/harness/src/lib/opencode/runner.ts +281 -0
- package/.allhands/harness/src/lib/oracle.ts +926 -0
- package/.allhands/harness/src/lib/planning-utils.ts +150 -0
- package/.allhands/harness/src/lib/planning.ts +605 -0
- package/.allhands/harness/src/lib/pr-review.ts +225 -0
- package/.allhands/harness/src/lib/prompts.ts +522 -0
- package/.allhands/harness/src/lib/schema.ts +418 -0
- package/.allhands/harness/src/lib/schemas/agent-profile.ts +141 -0
- package/.allhands/harness/src/lib/schemas/template-vars.ts +138 -0
- package/.allhands/harness/src/lib/session.ts +164 -0
- package/.allhands/harness/src/lib/specs.ts +348 -0
- package/.allhands/harness/src/lib/tldr.ts +829 -0
- package/.allhands/harness/src/lib/tmux.ts +1051 -0
- package/.allhands/harness/src/lib/trace-store.ts +714 -0
- package/.allhands/harness/src/mcp/__tests__/index.test.ts +46 -0
- package/.allhands/harness/src/mcp/_template.ts +47 -0
- package/.allhands/harness/src/mcp/filesystem.ts +33 -0
- package/.allhands/harness/src/mcp/index.ts +69 -0
- package/.allhands/harness/src/mcp/playwright.ts +34 -0
- package/.allhands/harness/src/mcp/xcodebuild.ts +29 -0
- package/.allhands/harness/src/schemas/docs.schema.json +44 -0
- package/.allhands/harness/src/schemas/settings.schema.json +214 -0
- package/.allhands/harness/src/tui/actions.ts +227 -0
- package/.allhands/harness/src/tui/file-viewer-modal.ts +270 -0
- package/.allhands/harness/src/tui/index.ts +1574 -0
- package/.allhands/harness/src/tui/modal.ts +232 -0
- package/.allhands/harness/src/tui/prompts-pane.ts +186 -0
- package/.allhands/harness/src/tui/status-pane.ts +434 -0
- package/.allhands/harness/tsconfig.json +22 -0
- package/.allhands/harness/vitest.config.ts +13 -0
- package/.allhands/pillars.md +33 -0
- package/.allhands/principles.md +88 -0
- package/.allhands/schemas/alignment.yaml +51 -0
- package/.allhands/schemas/documentation.yaml +10 -0
- package/.allhands/schemas/prompt.yaml +92 -0
- package/.allhands/schemas/skill.yaml +34 -0
- package/.allhands/schemas/solution.yaml +131 -0
- package/.allhands/schemas/spec.yaml +67 -0
- package/.allhands/schemas/validation-suite.yaml +49 -0
- package/.allhands/schemas/workflow.yaml +51 -0
- package/.allhands/settings.json +57 -0
- package/.allhands/skills/claude-code-patterns/SKILL.md +60 -0
- package/.allhands/skills/claude-code-patterns/docs/context-hygiene.md +19 -0
- package/.allhands/skills/harness-maintenance/SKILL.md +449 -0
- package/.allhands/skills/harness-maintenance/references/core-architecture.md +187 -0
- package/.allhands/skills/harness-maintenance/references/harness-skills.md +87 -0
- package/.allhands/skills/harness-maintenance/references/knowledge-compounding.md +78 -0
- package/.allhands/skills/harness-maintenance/references/tools-commands-mcp-hooks.md +115 -0
- package/.allhands/skills/harness-maintenance/references/validation-tooling.md +77 -0
- package/.allhands/skills/harness-maintenance/references/writing-flows.md +84 -0
- package/.allhands/validation/browser-automation.md +109 -0
- package/.allhands/validation/xcode-automation.md +195 -0
- package/.allhands/workflows/documentation.md +86 -0
- package/.allhands/workflows/investigation.md +81 -0
- package/.allhands/workflows/milestone.md +91 -0
- package/.allhands/workflows/optimization.md +85 -0
- package/.allhands/workflows/refactor.md +99 -0
- package/.allhands/workflows/triage.md +81 -0
- package/.claude/README.md +1 -0
- package/.claude/agents/explorer.md +10 -0
- package/.claude/agents/researcher.md +11 -0
- package/.claude/agents/task-runner.md +8 -0
- package/.claude/settings.json +231 -0
- package/.env.ai.example +7 -0
- package/.github/workflows/npm-publish.yml +69 -0
- package/.internal.json +45 -0
- package/.tldr/config.json +11 -0
- package/.tldrignore +90 -0
- package/CLAUDE.md +6 -0
- package/README.md +98 -0
- package/bin/sync-cli.js +7552 -0
- package/concerns.md +7 -0
- package/docs/README.md +41 -0
- package/docs/agents/README.md +24 -0
- package/docs/agents/agent-configuration-system.md +86 -0
- package/docs/agents/execution-agents.md +50 -0
- package/docs/agents/knowledge-agents.md +61 -0
- package/docs/agents/orchestration-agent.md +57 -0
- package/docs/agents/planning-agents.md +84 -0
- package/docs/agents/quality-review-agents.md +67 -0
- package/docs/agents/workflow-agent-orchestration.md +69 -0
- package/docs/flows/README.md +44 -0
- package/docs/flows/compounding.md +126 -0
- package/docs/flows/coordination.md +72 -0
- package/docs/flows/core-harness-integration.md +63 -0
- package/docs/flows/documentation-orchestration.md +98 -0
- package/docs/flows/e2e-test-plan-building.md +83 -0
- package/docs/flows/emergent-refinement.md +104 -0
- package/docs/flows/flow-authoring-and-mcp-tools.md +89 -0
- package/docs/flows/judge-reviewing.md +112 -0
- package/docs/flows/plan-deepening-and-research.md +107 -0
- package/docs/flows/plan-review-jury.md +114 -0
- package/docs/flows/pr-reviewing.md +54 -0
- package/docs/flows/prompt-task-execution.md +119 -0
- package/docs/flows/spec-planning.md +162 -0
- package/docs/flows/type-specific-scoping-flows.md +49 -0
- package/docs/flows/validation-and-skills-integration.md +145 -0
- package/docs/flows/wip/wip-flows.md +102 -0
- package/docs/harness/README.md +23 -0
- package/docs/harness/agent-profiles.md +84 -0
- package/docs/harness/cli/README.md +24 -0
- package/docs/harness/cli/cli-entry-and-command-discovery.md +91 -0
- package/docs/harness/cli/docs-command.md +87 -0
- package/docs/harness/cli/knowledge-command.md +91 -0
- package/docs/harness/cli/minor-cli-commands.md +65 -0
- package/docs/harness/cli/oracle-command.md +113 -0
- package/docs/harness/cli/planning-command.md +95 -0
- package/docs/harness/cli/schema-and-validation-commands.md +154 -0
- package/docs/harness/cli/search-commands.md +97 -0
- package/docs/harness/cli/spawn-command.md +136 -0
- package/docs/harness/cli/specs-command.md +102 -0
- package/docs/harness/cli/tools-command.md +122 -0
- package/docs/harness/cli/trace-command.md +122 -0
- package/docs/harness/cli-daemon.md +92 -0
- package/docs/harness/event-loop.md +184 -0
- package/docs/harness/hooks/README.md +15 -0
- package/docs/harness/hooks/context-hooks.md +96 -0
- package/docs/harness/hooks/lifecycle-and-observability-hooks.md +135 -0
- package/docs/harness/hooks/validation-hooks.md +97 -0
- package/docs/harness/test-harness.md +149 -0
- package/docs/harness/tui.md +176 -0
- package/docs/memories.md +20 -0
- package/docs/solutions/agentic-issues/premature-agent-deletion-tui-action-dependency-20260130.md +49 -0
- package/docs/solutions/agentic-issues/ref-anchor-scope-mismatch-skill-references-20260131.md +55 -0
- package/docs/solutions/agentic-issues/tautological-tests-routing-20260131.md +52 -0
- package/docs/solutions/integration_issue/blocktool-output-format-mismatch-hook-runner-20260130.md +52 -0
- package/docs/solutions/integration_issue/dual-validation-path-divergence-schema-20260130.md +66 -0
- package/docs/solutions/security-issues/unsanitized-domain-path-join-20260131.md +52 -0
- package/docs/solutions/test-failures/event-loop-mock-ordering-checkAgentWindows-20260130.md +63 -0
- package/docs/sync-cli/README.md +19 -0
- package/docs/sync-cli/cli-entrypoint-and-commands.md +39 -0
- package/docs/sync-cli/commands/README.md +11 -0
- package/docs/sync-cli/commands/pull-manifest-command.md +36 -0
- package/docs/sync-cli/commands/push-command.md +84 -0
- package/docs/sync-cli/commands/sync-command.md +71 -0
- package/docs/sync-cli/systems/README.md +14 -0
- package/docs/sync-cli/systems/git-and-github-integration.md +49 -0
- package/docs/sync-cli/systems/interactive-ui.md +43 -0
- package/docs/sync-cli/systems/manifest-and-distribution.md +51 -0
- package/docs/sync-cli/systems/path-resolution.md +42 -0
- package/package.json +46 -0
- package/scripts/install-shim.sh +40 -0
- package/scripts/pre-pack.sh +25 -0
- package/specs/harness-maintenance-skill.spec.md +138 -0
- package/specs/roadmap/git-spec-lifecycle-management.spec.md +113 -0
- package/specs/sync-init-flag.spec.md +117 -0
- package/specs/unified-workflow-orchestration.spec.md +250 -0
- package/specs/validation-tooling-practice.spec.md +98 -0
- package/specs/workflow-domain-configuration.spec.md +265 -0
- package/src/commands/pull-manifest.ts +31 -0
- package/src/commands/push.ts +344 -0
- package/src/commands/sync.ts +289 -0
- package/src/lib/constants.ts +10 -0
- package/src/lib/dotfiles.ts +36 -0
- package/src/lib/fs-utils.ts +18 -0
- package/src/lib/gh.ts +40 -0
- package/src/lib/git.ts +63 -0
- package/src/lib/gitignore.ts +167 -0
- package/src/lib/manifest.ts +121 -0
- package/src/lib/marker-sync.ts +39 -0
- package/src/lib/paths.ts +38 -0
- package/src/lib/target-lines.ts +66 -0
- package/src/lib/ui.ts +78 -0
- package/src/sync-cli.ts +120 -0
- package/target-lines.json +23 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge commands - semantic search and indexing for docs and roadmap.
|
|
3
|
+
*
|
|
4
|
+
* Commands:
|
|
5
|
+
* ah knowledge docs search <query> [--metadata-only]
|
|
6
|
+
* ah knowledge roadmap search <query> [--metadata-only]
|
|
7
|
+
* ah knowledge docs reindex
|
|
8
|
+
* ah knowledge roadmap reindex
|
|
9
|
+
* ah knowledge reindex (all indexes)
|
|
10
|
+
* ah knowledge status (all indexes)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { spawnSync } from "child_process";
|
|
14
|
+
import { Command } from "commander";
|
|
15
|
+
import { readFileSync } from "fs";
|
|
16
|
+
import { dirname, join } from "path";
|
|
17
|
+
import { fileURLToPath } from "url";
|
|
18
|
+
import {
|
|
19
|
+
AgentRunner,
|
|
20
|
+
type AggregatorOutput,
|
|
21
|
+
type SearchResult,
|
|
22
|
+
} from "../lib/opencode/index.js";
|
|
23
|
+
import {
|
|
24
|
+
INDEX_CONFIGS,
|
|
25
|
+
KnowledgeService,
|
|
26
|
+
type FileChange,
|
|
27
|
+
type IndexName,
|
|
28
|
+
} from "../lib/knowledge.js";
|
|
29
|
+
import { BaseCommand, CommandResult, tracedAction } from "../lib/base-command.js";
|
|
30
|
+
import { getBaseBranch } from "../lib/git.js";
|
|
31
|
+
|
|
32
|
+
const getProjectRoot = (): string => {
|
|
33
|
+
return process.env.PROJECT_ROOT || process.cwd();
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Load aggregator prompt from file
|
|
37
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
const AGGREGATOR_PROMPT_PATH = join(__dirname, "../lib/opencode/prompts/knowledge-aggregator.md");
|
|
39
|
+
|
|
40
|
+
const getAggregatorPrompt = (): string => {
|
|
41
|
+
return readFileSync(AGGREGATOR_PROMPT_PATH, "utf-8");
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const DEFAULT_TOKEN_THRESHOLD = 3500;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Auto-detect file changes since branch diverged from base for a specific index.
|
|
48
|
+
*/
|
|
49
|
+
function getChangesFromGit(indexName: IndexName): FileChange[] {
|
|
50
|
+
const config = INDEX_CONFIGS[indexName];
|
|
51
|
+
if (!config) return [];
|
|
52
|
+
|
|
53
|
+
const baseBranch = getBaseBranch();
|
|
54
|
+
const cwd = getProjectRoot();
|
|
55
|
+
|
|
56
|
+
// Get merge-base commit
|
|
57
|
+
const mergeBaseResult = spawnSync("git", ["merge-base", baseBranch, "HEAD"], {
|
|
58
|
+
encoding: "utf-8",
|
|
59
|
+
cwd,
|
|
60
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (mergeBaseResult.status !== 0) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const mergeBase = mergeBaseResult.stdout.trim();
|
|
68
|
+
|
|
69
|
+
// Get changed files since merge-base, filtered to index paths
|
|
70
|
+
const diffResult = spawnSync(
|
|
71
|
+
"git",
|
|
72
|
+
["diff", "--name-status", `${mergeBase}..HEAD`, "--", ...config.paths],
|
|
73
|
+
{ encoding: "utf-8", cwd, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (diffResult.status !== 0 || !diffResult.stdout.trim()) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const changes: FileChange[] = [];
|
|
81
|
+
const lines = diffResult.stdout.trim().split("\n");
|
|
82
|
+
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
const [status, filePath] = line.split("\t");
|
|
85
|
+
if (!filePath) continue;
|
|
86
|
+
|
|
87
|
+
// Check extension
|
|
88
|
+
const ext = "." + filePath.split(".").pop();
|
|
89
|
+
if (!config.extensions.includes(ext)) continue;
|
|
90
|
+
|
|
91
|
+
// Skip memories.md (project-specific learnings, not indexed)
|
|
92
|
+
if (filePath.endsWith("memories.md")) continue;
|
|
93
|
+
|
|
94
|
+
if (status === "A") {
|
|
95
|
+
changes.push({ path: filePath, added: true });
|
|
96
|
+
} else if (status === "M") {
|
|
97
|
+
changes.push({ path: filePath, modified: true });
|
|
98
|
+
} else if (status === "D") {
|
|
99
|
+
changes.push({ path: filePath, deleted: true });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return changes;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Search command - searches a specific index
|
|
108
|
+
*/
|
|
109
|
+
class SearchCommand extends BaseCommand {
|
|
110
|
+
readonly name = "search";
|
|
111
|
+
readonly description: string;
|
|
112
|
+
private readonly indexName: IndexName;
|
|
113
|
+
|
|
114
|
+
constructor(indexName: IndexName) {
|
|
115
|
+
super();
|
|
116
|
+
this.indexName = indexName;
|
|
117
|
+
this.description = `Semantic search ${indexName}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
defineArguments(cmd: Command): void {
|
|
121
|
+
cmd
|
|
122
|
+
.argument("<query>", "Descriptive phrase (e.g. 'how to handle API authentication')")
|
|
123
|
+
.option("--metadata-only", "Return only file paths and descriptions (no full content)")
|
|
124
|
+
.option("--no-aggregate", "Disable aggregation entirely");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async execute(args: Record<string, unknown>): Promise<CommandResult> {
|
|
128
|
+
const query = args.query as string;
|
|
129
|
+
const metadataOnly = !!args.metadataOnly;
|
|
130
|
+
const noAggregate = !!args.noAggregate;
|
|
131
|
+
|
|
132
|
+
if (!query) {
|
|
133
|
+
return this.error("validation_error", "query is required");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const projectRoot = getProjectRoot();
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const service = new KnowledgeService(projectRoot);
|
|
140
|
+
const results = await service.search(this.indexName, query, 50, metadataOnly);
|
|
141
|
+
|
|
142
|
+
// Skip aggregation if metadata-only or explicitly disabled
|
|
143
|
+
if (metadataOnly || noAggregate) {
|
|
144
|
+
return this.success({
|
|
145
|
+
index: this.indexName,
|
|
146
|
+
query,
|
|
147
|
+
metadata_only: metadataOnly,
|
|
148
|
+
results,
|
|
149
|
+
result_count: results.length,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check if aggregation is needed (token threshold)
|
|
154
|
+
const totalTokens = results.reduce((sum, r) => sum + r.token_count, 0);
|
|
155
|
+
if (totalTokens <= DEFAULT_TOKEN_THRESHOLD) {
|
|
156
|
+
return this.success({
|
|
157
|
+
index: this.indexName,
|
|
158
|
+
query,
|
|
159
|
+
results,
|
|
160
|
+
result_count: results.length,
|
|
161
|
+
aggregated: false,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Aggregate with AI
|
|
166
|
+
const runner = new AgentRunner(projectRoot);
|
|
167
|
+
const { fullResults, minimizedResults } = results.reduce(
|
|
168
|
+
(acc, r) => {
|
|
169
|
+
if (r.full_resource_context) {
|
|
170
|
+
acc.fullResults.push(r);
|
|
171
|
+
} else {
|
|
172
|
+
acc.minimizedResults.push(r);
|
|
173
|
+
}
|
|
174
|
+
return acc;
|
|
175
|
+
},
|
|
176
|
+
{ fullResults: [] as SearchResult[], minimizedResults: [] as SearchResult[] }
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const userMessage = JSON.stringify({
|
|
180
|
+
query,
|
|
181
|
+
full_results: fullResults,
|
|
182
|
+
minimized_results: minimizedResults.map(r => ({
|
|
183
|
+
resource_path: r.resource_path,
|
|
184
|
+
similarity: r.similarity,
|
|
185
|
+
description: r.description,
|
|
186
|
+
relevant_files: r.relevant_files,
|
|
187
|
+
})),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const agentResult = await runner.run<AggregatorOutput>(
|
|
191
|
+
{
|
|
192
|
+
name: "knowledge-aggregator",
|
|
193
|
+
systemPrompt: getAggregatorPrompt(),
|
|
194
|
+
timeoutMs: 60000,
|
|
195
|
+
steps: 5,
|
|
196
|
+
},
|
|
197
|
+
userMessage
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
if (!agentResult.success) {
|
|
201
|
+
// Fall back to raw results on aggregation failure
|
|
202
|
+
return this.success({
|
|
203
|
+
index: this.indexName,
|
|
204
|
+
query,
|
|
205
|
+
results,
|
|
206
|
+
result_count: results.length,
|
|
207
|
+
aggregated: false,
|
|
208
|
+
aggregation_error: agentResult.error,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return this.success({
|
|
213
|
+
index: this.indexName,
|
|
214
|
+
query,
|
|
215
|
+
aggregated: true,
|
|
216
|
+
insight: agentResult.data!.insight,
|
|
217
|
+
lsp_entry_points: agentResult.data!.lsp_entry_points,
|
|
218
|
+
design_notes: agentResult.data!.design_notes,
|
|
219
|
+
source_results: results.length,
|
|
220
|
+
});
|
|
221
|
+
} catch (error) {
|
|
222
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
223
|
+
return this.error("search_error", message);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Reindex command - rebuilds an index
|
|
230
|
+
*/
|
|
231
|
+
class ReindexCommand extends BaseCommand {
|
|
232
|
+
readonly name = "reindex";
|
|
233
|
+
readonly description: string;
|
|
234
|
+
private readonly indexName: IndexName | "all";
|
|
235
|
+
|
|
236
|
+
constructor(indexName: IndexName | "all") {
|
|
237
|
+
super();
|
|
238
|
+
this.indexName = indexName;
|
|
239
|
+
this.description = indexName === "all"
|
|
240
|
+
? "Rebuild all indexes"
|
|
241
|
+
: `Rebuild ${indexName} index`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
defineArguments(cmd: Command): void {
|
|
245
|
+
cmd.option("--from-changes", "Only reindex changed files since branch diverged from base");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async execute(args: Record<string, unknown>): Promise<CommandResult> {
|
|
249
|
+
const fromChanges = !!args.fromChanges;
|
|
250
|
+
const projectRoot = getProjectRoot();
|
|
251
|
+
const service = new KnowledgeService(projectRoot);
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
if (this.indexName === "all") {
|
|
255
|
+
if (fromChanges) {
|
|
256
|
+
const results: Record<string, unknown> = {};
|
|
257
|
+
for (const name of KnowledgeService.getIndexNames()) {
|
|
258
|
+
const changes = getChangesFromGit(name as IndexName);
|
|
259
|
+
if (changes.length > 0) {
|
|
260
|
+
results[name] = await service.reindexFromChanges(name as IndexName, changes);
|
|
261
|
+
} else {
|
|
262
|
+
results[name] = { skipped: true, reason: "no changes detected" };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return this.success({ indexes: results });
|
|
266
|
+
} else {
|
|
267
|
+
const results = await service.reindexAllIndexes();
|
|
268
|
+
return this.success({ indexes: results });
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
if (fromChanges) {
|
|
272
|
+
const changes = getChangesFromGit(this.indexName);
|
|
273
|
+
if (changes.length === 0) {
|
|
274
|
+
return this.success({
|
|
275
|
+
index: this.indexName,
|
|
276
|
+
skipped: true,
|
|
277
|
+
reason: "no changes detected"
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
const result = await service.reindexFromChanges(this.indexName, changes);
|
|
281
|
+
return this.success({ index: this.indexName, ...result });
|
|
282
|
+
} else {
|
|
283
|
+
const result = await service.reindexAll(this.indexName);
|
|
284
|
+
return this.success({ index: this.indexName, ...result });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} catch (error) {
|
|
288
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
289
|
+
return this.error("reindex_error", message);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Status command - check index status
|
|
296
|
+
*/
|
|
297
|
+
class StatusCommand extends BaseCommand {
|
|
298
|
+
readonly name = "status";
|
|
299
|
+
readonly description = "Check status of all indexes";
|
|
300
|
+
|
|
301
|
+
defineArguments(_cmd: Command): void {
|
|
302
|
+
// No arguments
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async execute(_args: Record<string, unknown>): Promise<CommandResult> {
|
|
306
|
+
const projectRoot = getProjectRoot();
|
|
307
|
+
const service = new KnowledgeService(projectRoot);
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
const results = await service.checkAllIndexes();
|
|
311
|
+
return this.success({ indexes: results });
|
|
312
|
+
} catch (error) {
|
|
313
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
314
|
+
return this.error("status_error", message);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Register knowledge commands on the given commander program.
|
|
321
|
+
*/
|
|
322
|
+
export function register(program: Command): void {
|
|
323
|
+
const knowledgeCmd = program
|
|
324
|
+
.command("knowledge")
|
|
325
|
+
.description("Semantic search and indexing for docs and specs");
|
|
326
|
+
|
|
327
|
+
// Create subcommand for each index (docs, specs)
|
|
328
|
+
for (const indexName of KnowledgeService.getIndexNames()) {
|
|
329
|
+
const indexCmd = knowledgeCmd
|
|
330
|
+
.command(indexName)
|
|
331
|
+
.description(`${INDEX_CONFIGS[indexName].description} operations`);
|
|
332
|
+
|
|
333
|
+
// Search command
|
|
334
|
+
const searchCmd = new SearchCommand(indexName as IndexName);
|
|
335
|
+
const searchSubCmd = indexCmd.command(searchCmd.name).description(searchCmd.description);
|
|
336
|
+
searchCmd.defineArguments(searchSubCmd);
|
|
337
|
+
searchSubCmd.action(tracedAction(`knowledge ${indexName} search`, async (...args) => {
|
|
338
|
+
const opts = args[args.length - 2] as Record<string, unknown>;
|
|
339
|
+
const cmdObj = args[args.length - 1] as Command;
|
|
340
|
+
const positionalArgs = cmdObj.args;
|
|
341
|
+
const namedArgs: Record<string, unknown> = { ...opts };
|
|
342
|
+
if (positionalArgs[0]) namedArgs.query = positionalArgs[0];
|
|
343
|
+
const result = await searchCmd.execute(namedArgs);
|
|
344
|
+
console.log(JSON.stringify(result, null, 2));
|
|
345
|
+
// Exit explicitly - ONNX runtime thread pools don't auto-cleanup
|
|
346
|
+
process.exit(0);
|
|
347
|
+
}));
|
|
348
|
+
|
|
349
|
+
// Reindex command
|
|
350
|
+
const reindexCmd = new ReindexCommand(indexName as IndexName);
|
|
351
|
+
const reindexSubCmd = indexCmd.command(reindexCmd.name).description(reindexCmd.description);
|
|
352
|
+
reindexCmd.defineArguments(reindexSubCmd);
|
|
353
|
+
reindexSubCmd.action(tracedAction(`knowledge ${indexName} reindex`, async (...args) => {
|
|
354
|
+
const opts = args[args.length - 2] as Record<string, unknown>;
|
|
355
|
+
const result = await reindexCmd.execute(opts);
|
|
356
|
+
console.log(JSON.stringify(result, null, 2));
|
|
357
|
+
// Exit explicitly - ONNX runtime thread pools don't auto-cleanup
|
|
358
|
+
process.exit(0);
|
|
359
|
+
}));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Global reindex command
|
|
363
|
+
const globalReindexCmd = new ReindexCommand("all");
|
|
364
|
+
const globalReindexSubCmd = knowledgeCmd.command("reindex").description(globalReindexCmd.description);
|
|
365
|
+
globalReindexCmd.defineArguments(globalReindexSubCmd);
|
|
366
|
+
globalReindexSubCmd.action(tracedAction('knowledge reindex', async (...args) => {
|
|
367
|
+
const opts = args[args.length - 2] as Record<string, unknown>;
|
|
368
|
+
const result = await globalReindexCmd.execute(opts);
|
|
369
|
+
console.log(JSON.stringify(result, null, 2));
|
|
370
|
+
// Exit explicitly - ONNX runtime thread pools don't auto-cleanup
|
|
371
|
+
process.exit(0);
|
|
372
|
+
}));
|
|
373
|
+
|
|
374
|
+
// Status command
|
|
375
|
+
const statusCmd = new StatusCommand();
|
|
376
|
+
const statusSubCmd = knowledgeCmd.command(statusCmd.name).description(statusCmd.description);
|
|
377
|
+
statusCmd.defineArguments(statusSubCmd);
|
|
378
|
+
statusSubCmd.action(tracedAction('knowledge status', async () => {
|
|
379
|
+
const result = await statusCmd.execute({});
|
|
380
|
+
console.log(JSON.stringify(result, null, 2));
|
|
381
|
+
}));
|
|
382
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memories Command (Agent-Facing)
|
|
3
|
+
*
|
|
4
|
+
* Keyword-based search for memories in docs/memories.md.
|
|
5
|
+
* Parses markdown tables and searches across name, domain, source, description fields.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ah memories search <query> Search memories by keywords
|
|
9
|
+
* ah memories search <query> --domain planning Filter by domain
|
|
10
|
+
* ah memories search <query> --source user-steering Filter by source
|
|
11
|
+
* ah memories list List memory sections with counts
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Command } from 'commander';
|
|
15
|
+
import { existsSync, readFileSync } from 'fs';
|
|
16
|
+
import { join } from 'path';
|
|
17
|
+
import { tracedAction } from '../lib/base-command.js';
|
|
18
|
+
|
|
19
|
+
const getProjectRoot = (): string => {
|
|
20
|
+
return process.env.PROJECT_ROOT || process.cwd();
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const getMemoriesPath = (): string => {
|
|
24
|
+
return join(getProjectRoot(), 'docs', 'memories.md');
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
interface MemoryEntry {
|
|
28
|
+
name: string;
|
|
29
|
+
domain: string;
|
|
30
|
+
source: string;
|
|
31
|
+
description: string;
|
|
32
|
+
specSection: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface MemoryMatch extends MemoryEntry {
|
|
36
|
+
score: number;
|
|
37
|
+
matchedFields: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface MemorySection {
|
|
41
|
+
name: string;
|
|
42
|
+
count: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Extract keywords from a search query.
|
|
47
|
+
* Handles quoted phrases and splits on whitespace.
|
|
48
|
+
*/
|
|
49
|
+
function extractKeywords(query: string): string[] {
|
|
50
|
+
const keywords: string[] = [];
|
|
51
|
+
|
|
52
|
+
// Extract quoted phrases first
|
|
53
|
+
const quotedRegex = /"([^"]+)"/g;
|
|
54
|
+
let match;
|
|
55
|
+
while ((match = quotedRegex.exec(query)) !== null) {
|
|
56
|
+
keywords.push(match[1].toLowerCase());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Remove quoted phrases and split remaining on whitespace
|
|
60
|
+
const remaining = query.replace(quotedRegex, '').trim();
|
|
61
|
+
if (remaining) {
|
|
62
|
+
keywords.push(...remaining.toLowerCase().split(/\s+/).filter(k => k.length > 0));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return keywords;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Parse markdown tables from memories.md into structured entries.
|
|
70
|
+
* Expects tables with columns: Name, Domain, Source, Description
|
|
71
|
+
* grouped under ## section headers.
|
|
72
|
+
*/
|
|
73
|
+
function parseMemories(filePath: string): MemoryEntry[] {
|
|
74
|
+
if (!existsSync(filePath)) return [];
|
|
75
|
+
|
|
76
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
77
|
+
const lines = content.split('\n');
|
|
78
|
+
const entries: MemoryEntry[] = [];
|
|
79
|
+
let currentSection = 'unknown';
|
|
80
|
+
|
|
81
|
+
for (let i = 0; i < lines.length; i++) {
|
|
82
|
+
const line = lines[i].trim();
|
|
83
|
+
|
|
84
|
+
// Track section headers
|
|
85
|
+
if (line.startsWith('## ')) {
|
|
86
|
+
currentSection = line.replace('## ', '').trim();
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Skip non-table lines, header rows, and separator rows
|
|
91
|
+
if (!line.startsWith('|') || line.includes('---') || /\|\s*Name\s*\|/i.test(line)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Parse table row
|
|
96
|
+
const cells = line.split('|').map(c => c.trim()).filter(c => c.length > 0);
|
|
97
|
+
if (cells.length >= 4) {
|
|
98
|
+
entries.push({
|
|
99
|
+
name: cells[0],
|
|
100
|
+
domain: cells[1],
|
|
101
|
+
source: cells[2],
|
|
102
|
+
description: cells[3],
|
|
103
|
+
specSection: currentSection,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return entries;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get section names and entry counts from memories file.
|
|
113
|
+
*/
|
|
114
|
+
function getMemorySections(filePath: string): MemorySection[] {
|
|
115
|
+
if (!existsSync(filePath)) return [];
|
|
116
|
+
|
|
117
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
118
|
+
const lines = content.split('\n');
|
|
119
|
+
const sections: MemorySection[] = [];
|
|
120
|
+
let currentSection = '';
|
|
121
|
+
let currentCount = 0;
|
|
122
|
+
|
|
123
|
+
for (const line of lines) {
|
|
124
|
+
const trimmed = line.trim();
|
|
125
|
+
|
|
126
|
+
if (trimmed.startsWith('## ')) {
|
|
127
|
+
if (currentSection) {
|
|
128
|
+
sections.push({ name: currentSection, count: currentCount });
|
|
129
|
+
}
|
|
130
|
+
currentSection = trimmed.replace('## ', '').trim();
|
|
131
|
+
currentCount = 0;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Count data rows (not headers or separators)
|
|
136
|
+
if (currentSection && trimmed.startsWith('|') && !trimmed.includes('---') && !/\|\s*Name\s*\|/i.test(trimmed)) {
|
|
137
|
+
const cells = trimmed.split('|').map(c => c.trim()).filter(c => c.length > 0);
|
|
138
|
+
if (cells.length >= 4) {
|
|
139
|
+
currentCount++;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Push final section
|
|
145
|
+
if (currentSection) {
|
|
146
|
+
sections.push({ name: currentSection, count: currentCount });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return sections;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Score how well a memory matches the search keywords.
|
|
154
|
+
*/
|
|
155
|
+
function scoreMemory(entry: MemoryEntry, keywords: string[]): { score: number; matchedFields: string[] } {
|
|
156
|
+
let score = 0;
|
|
157
|
+
const matchedFields: string[] = [];
|
|
158
|
+
|
|
159
|
+
for (const keyword of keywords) {
|
|
160
|
+
// Name match (high weight)
|
|
161
|
+
if (entry.name.toLowerCase().includes(keyword)) {
|
|
162
|
+
score += 3;
|
|
163
|
+
if (!matchedFields.includes('name')) matchedFields.push('name');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Description match (medium weight)
|
|
167
|
+
if (entry.description.toLowerCase().includes(keyword)) {
|
|
168
|
+
score += 2;
|
|
169
|
+
if (!matchedFields.includes('description')) matchedFields.push('description');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Domain match (medium weight)
|
|
173
|
+
if (entry.domain.toLowerCase().includes(keyword)) {
|
|
174
|
+
score += 2;
|
|
175
|
+
if (!matchedFields.includes('domain')) matchedFields.push('domain');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Source match (low weight)
|
|
179
|
+
if (entry.source.toLowerCase().includes(keyword)) {
|
|
180
|
+
score += 1;
|
|
181
|
+
if (!matchedFields.includes('source')) matchedFields.push('source');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { score, matchedFields };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Search memories matching a query with optional filters.
|
|
190
|
+
*/
|
|
191
|
+
function searchMemories(
|
|
192
|
+
query: string,
|
|
193
|
+
options: { domain?: string; source?: string; limit?: number }
|
|
194
|
+
): MemoryMatch[] {
|
|
195
|
+
const memoriesPath = getMemoriesPath();
|
|
196
|
+
const keywords = extractKeywords(query);
|
|
197
|
+
const limit = options.limit ?? 10;
|
|
198
|
+
|
|
199
|
+
if (keywords.length === 0) return [];
|
|
200
|
+
|
|
201
|
+
let entries = parseMemories(memoriesPath);
|
|
202
|
+
|
|
203
|
+
// Apply filters
|
|
204
|
+
if (options.domain) {
|
|
205
|
+
entries = entries.filter(e => e.domain.toLowerCase() === options.domain!.toLowerCase());
|
|
206
|
+
}
|
|
207
|
+
if (options.source) {
|
|
208
|
+
entries = entries.filter(e => e.source.toLowerCase() === options.source!.toLowerCase());
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const matches: MemoryMatch[] = [];
|
|
212
|
+
|
|
213
|
+
for (const entry of entries) {
|
|
214
|
+
const { score, matchedFields } = scoreMemory(entry, keywords);
|
|
215
|
+
if (score > 0) {
|
|
216
|
+
matches.push({ ...entry, score, matchedFields });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Sort by score descending
|
|
221
|
+
matches.sort((a, b) => b.score - a.score);
|
|
222
|
+
|
|
223
|
+
return matches.slice(0, limit);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function register(program: Command): void {
|
|
227
|
+
const memoriesCmd = program
|
|
228
|
+
.command('memories')
|
|
229
|
+
.description('Search and browse project memories');
|
|
230
|
+
|
|
231
|
+
// Search command
|
|
232
|
+
memoriesCmd
|
|
233
|
+
.command('search <query>')
|
|
234
|
+
.description('Search memories by keywords (searches name, domain, source, description)')
|
|
235
|
+
.option('--domain <domain>', 'Filter by domain (planning, validation, implementation, harness-tooling, ideation)')
|
|
236
|
+
.option('--source <source>', 'Filter by source (user-steering, agent-inferred)')
|
|
237
|
+
.option('--limit <n>', 'Maximum number of results', '10')
|
|
238
|
+
.action(tracedAction('memories search', async (query: string, options: { domain?: string; source?: string; limit?: string }) => {
|
|
239
|
+
const limit = parseInt(options.limit || '10', 10);
|
|
240
|
+
const matches = searchMemories(query, {
|
|
241
|
+
domain: options.domain,
|
|
242
|
+
source: options.source,
|
|
243
|
+
limit,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
if (matches.length === 0) {
|
|
247
|
+
console.log(JSON.stringify({
|
|
248
|
+
success: true,
|
|
249
|
+
query,
|
|
250
|
+
keywords: extractKeywords(query),
|
|
251
|
+
results: [],
|
|
252
|
+
message: 'No matching memories found',
|
|
253
|
+
}, null, 2));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const results = matches.map(match => ({
|
|
258
|
+
name: match.name,
|
|
259
|
+
domain: match.domain,
|
|
260
|
+
source: match.source,
|
|
261
|
+
description: match.description,
|
|
262
|
+
spec_section: match.specSection,
|
|
263
|
+
score: match.score,
|
|
264
|
+
matched_fields: match.matchedFields,
|
|
265
|
+
}));
|
|
266
|
+
|
|
267
|
+
console.log(JSON.stringify({
|
|
268
|
+
success: true,
|
|
269
|
+
query,
|
|
270
|
+
keywords: extractKeywords(query),
|
|
271
|
+
result_count: results.length,
|
|
272
|
+
results,
|
|
273
|
+
}, null, 2));
|
|
274
|
+
}));
|
|
275
|
+
|
|
276
|
+
// List command
|
|
277
|
+
memoriesCmd
|
|
278
|
+
.command('list')
|
|
279
|
+
.description('List memory sections with entry counts')
|
|
280
|
+
.action(tracedAction('memories list', async () => {
|
|
281
|
+
const memoriesPath = getMemoriesPath();
|
|
282
|
+
|
|
283
|
+
if (!existsSync(memoriesPath)) {
|
|
284
|
+
console.log(JSON.stringify({
|
|
285
|
+
success: true,
|
|
286
|
+
message: 'No memories file found. Create docs/memories.md to start capturing learnings.',
|
|
287
|
+
sections: [],
|
|
288
|
+
total_entries: 0,
|
|
289
|
+
}, null, 2));
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const sections = getMemorySections(memoriesPath);
|
|
294
|
+
const totalEntries = sections.reduce((sum, s) => sum + s.count, 0);
|
|
295
|
+
|
|
296
|
+
console.log(JSON.stringify({
|
|
297
|
+
success: true,
|
|
298
|
+
sections,
|
|
299
|
+
total_entries: totalEntries,
|
|
300
|
+
}, null, 2));
|
|
301
|
+
}));
|
|
302
|
+
}
|