-
Notifications
You must be signed in to change notification settings - Fork 2
Coding Guide
Code style, conventions, and best practices for the GolemCore Bot codebase.
This project follows Conventional Commits 1.0.0.
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
| Type | When to use |
|---|---|
feat |
New feature or capability |
fix |
Bug fix |
refactor |
Code change that neither fixes a bug nor adds a feature |
test |
Adding or updating tests |
docs |
Documentation only |
chore |
Build config, CI, dependencies, tooling |
perf |
Performance improvement |
style |
Formatting, whitespace (no logic change) |
Use the module or area name: llm, telegram, tools, skills, mcp, auto, routing, security, storage, loop.
feat(tools): add BrowserTool screenshot mode
fix(llm): handle empty response from Anthropic API
refactor(routing): extract MessageContextAggregator from SkillRoutingSystem
test(mcp): add McpClient lifecycle tests
chore: upgrade langchain4j to 1.11.0
feat(skills)!: rename nextSkill field to next_skill in YAML frontmatter
BREAKING CHANGE: skill YAML files must use next_skill instead of nextSkill.
- Use imperative mood: "add feature", not "added feature" or "adds feature"
- First line under 72 characters
- No period at the end of the subject line
- Breaking changes: append
!after type/scope and add aBREAKING CHANGE:footer
Always declare variable types explicitly. Do not use var.
// Correct
List<Skill> available = getAvailableSkills();
String sessionId = buildSessionId(channelType, chatId);
Map<String, Skill> registry = new ConcurrentHashMap<>();
// Incorrect
var available = getAvailableSkills();All Spring-managed beans use constructor injection via Lombok's @RequiredArgsConstructor. Field injection (@Autowired) is prohibited.
@Service
@RequiredArgsConstructor
@Slf4j
public class SessionService implements SessionPort {
private final StoragePort storagePort;
private final ObjectMapper objectMapper;
private final Clock clock;
}@Lazy is prohibited. Break circular dependencies by:
- Extracting a shared interface/service
- Using
ApplicationEventPublisherfor one-way notifications - Moving the dependency into a method parameter
@Service
@RequiredArgsConstructor
@Slf4j
public class ExampleService {
// 1. Static constants
private static final String DIR_NAME = "examples";
// 2. Injected dependencies (private final)
private final StoragePort storagePort;
// 3. Mutable state (caches, registries)
private final Map<String, Item> cache = new ConcurrentHashMap<>();
// 4. @PostConstruct
// 5. Public interface methods (@Override)
// 6. Public methods
// 7. Private methods
}Classes:
| Suffix | Layer | Example |
|---|---|---|
*Service |
Domain services | SessionService |
*System |
Pipeline systems | ToolLoopExecutionSystem |
*Tool |
Tool implementations | FileSystemTool |
*Adapter |
Outbound adapters | Langchain4jAdapter |
*Port |
Port interfaces |
LlmPort, StoragePort
|
*Component |
Component interfaces | ToolComponent |
Methods:
| Pattern | Purpose | Example |
|---|---|---|
get* |
Retrieve, throw if missing | getSession() |
find* |
Lookup, return Optional | findByName() |
is*, has*
|
Boolean query |
isEnabled(), hasMcp()
|
create*, build*
|
Factory | createSession() |
process |
Pipeline processing | system.process(context) |
execute |
Run an action | tool.execute(params) |
No wildcard imports. Static imports allowed in tests only.
// Correct
import java.util.List;
import java.util.Map;
// Avoid
import java.util.*;| Annotation | Where | Purpose |
|---|---|---|
@RequiredArgsConstructor |
Services, adapters, systems, tools | Constructor injection |
@Slf4j |
Any class that logs | Generates log field |
@Data |
Domain model POJOs | Getters, setters, equals, hashCode, toString |
@Builder |
Domain models, request/response objects | Builder pattern |
@NoArgsConstructor |
Models deserialized by Jackson | Required for JSON/YAML parsing |
@AllArgsConstructor |
Models with @NoArgsConstructor
|
Complete constructor |
Computed getters in @Data classes get serialized by Jackson. Mark them @JsonIgnore:
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Goal {
private String id;
@Builder.Default
private List<AutoTask> tasks = new ArrayList<>();
@JsonIgnore
public long getCompletedTaskCount() {
return tasks.stream()
.filter(t -> t.getStatus() == TaskStatus.COMPLETED)
.count();
}
}All beans always exist at runtime. Use isEnabled() for runtime enable/disable — never @ConditionalOnProperty.
-
@Service— domain services -
@Component— adapters, tools, infrastructure -
@Configuration— config classes;@Beanmethods with injected fields must bestatic
Use @Slf4j. Parametrized messages only — no string concatenation.
| Level | Use for |
|---|---|
error |
Failures that need attention (with exception) |
warn |
Recoverable issues (rate limit, fallback) |
info |
Milestones (session created, skill matched) |
debug |
Internal flow (timing, cache hits) |
trace |
Very detailed (raw content) |
Use [Area] prefix:
log.info("[AutoMode] Created goal '{}'", title);
log.debug("[MCP] Starting server for skill: {}", skillName);- No custom exception hierarchy — use
IllegalStateException,IllegalArgumentException - Use
Optionalfor lookups, never returnnullfrom public methods - Catch broadly in I/O layers with
// NOSONARcomment - Log at appropriate level:
debugfor expected failures,errorfor unexpected
- Test class:
*Testsuffix - Method:
shouldDoSomethingWhenCondition()— notestprefix - Pattern: Arrange-Act-Assert
- Mocking: Mockito, create mocks in
@BeforeEach - Varargs mock: use custom
Answeron mock creation - Use
@ParameterizedTest+@ValueSourcefor input validation
@Test
void shouldRejectPathTraversalAttempt() {
// Arrange
Map<String, Object> params = Map.of("path", "../../../etc/passwd");
// Act
ToolResult result = tool.execute(params).join();
// Assert
assertTrue(result.getError().contains("traversal"));
}domain/ -> port/ OK
adapter/ -> port/ OK
adapter/ -> domain/ OK (models and services only)
domain/ -> adapter/ PROHIBITED
All tools implement ToolComponent. Use ToolResult.success(output) / ToolResult.failure(error).
Tools using AgentContextHolder (ThreadLocal) must NOT use CompletableFuture.supplyAsync().
Domain models use @Builder. AgentContext has no no-arg constructor — always use the builder:
AgentContext context = AgentContext.builder()
.session(session).messages(messages)
.channel(channelPort).chatId("123")
.build();./mvnw clean package -DskipTests # build
./mvnw test # run tests
./mvnw clean verify -P strict # full check (tests + PMD + SpotBugs)- Architecture — System design and pipeline
- Contributing — Development workflow and PR process
- Configuration — All settings
GolemCore Bot -- Apache License 2.0 | GitHub | Issues | Discussions
Getting Started
Core Concepts
Features
Reference
Development