feat: implement BOOT.md execution at daemon startup (#33)#43
Conversation
Add BootExecutor that discovers and runs BOOT.md files during daemon startup for workspace initialization. BOOT.md contains natural-language instructions executed autonomously before the daemon accepts connections. - BootExecutor: discovers project (.aceclaw/BOOT.md) and global (~/.aceclaw/BOOT.md) files, executes both in order with timeout - BootPermissionChecker: read-only tools auto-approved (read_file, glob, grep, list_directory), all write/execute tools denied during boot - AceClawConfig: bootEnabled (default true), bootTimeoutSeconds (default 120) - Integration in AceClawDaemon.start() between health monitor and UDS listener - 11 integration tests covering discovery, execution, permissions, failures
📝 WalkthroughWalkthroughAdds configurable boot-time BOOT.md execution: new boot config options, daemon startup integration to run boot sequences, a BootExecutor that discovers and executes project/global BOOT.md files with read-only tool restrictions and timeout guards, and tests covering discovery and execution scenarios. Changes
Sequence DiagramsequenceDiagram
participant Daemon as AceClawDaemon
participant BE as BootExecutor
participant FS as FileSystem
participant LLM as LLM Client
participant AR as AgentLoop
participant TR as ToolRegistry
participant PC as BootPermissionChecker
Daemon->>BE: execute(homeDir, workingDir, llmClient, toolRegistry, model, systemPrompt, ...)
BE->>FS: discover BOOT.md files (project -> global)
FS-->>BE: list of boot files
loop for each boot file
BE->>FS: read boot file
FS-->>BE: content
alt content empty
BE->>BE: mark found, skip execution
else content present
BE->>AR: create StreamingAgentLoop with SilentStreamHandler
AR->>LLM: send boot prompt (includes [BOOT] context)
LLM-->>AR: response
loop agent iterations
AR->>TR: request tool use
TR->>PC: check(toolName, input)
PC-->>TR: allow (read-only) or deny (with message)
alt allowed
AR->>LLM: continue interaction
else denied
AR->>AR: record denial, continue best-effort
end
end
AR-->>BE: execution result
end
BE->>BE: append per-file outcome to summary
end
BE-->>Daemon: return BootResult(executed, filesFound, summary, elapsed)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
Greptile SummaryImplements Major changes:
Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant D as AceClawDaemon
participant BE as BootExecutor
participant AL as StreamingAgentLoop
participant PC as BootPermissionChecker
participant LLM as LlmClient
participant FS as FileSystem
D->>D: start()
D->>D: Start health monitor
alt bootEnabled == true
D->>BE: execute(homeDir, workingDir, llmClient, ...)
BE->>FS: discoverBootFiles(homeDir, workingDir)
FS-->>BE: List of BOOT.md files (project first, then global)
alt No BOOT.md found
BE-->>D: BootResult(executed=false)
else BOOT.md files found
BE->>BE: Create BootPermissionChecker
BE->>AL: new StreamingAgentLoop(with bootPermChecker)
loop For each BOOT.md file
BE->>FS: Files.readString(bootFile)
FS-->>BE: BOOT.md content
alt Content is blank
BE->>BE: Skip (no LLM call)
else Content not blank
BE->>AL: runTurn("[BOOT] ..." + content)
AL->>LLM: streamMessage(bootPrompt)
loop Agent ReAct loop (max 10 iterations)
LLM-->>AL: Tool use request
AL->>PC: check(toolName, inputJson)
alt Read-only tool (read_file, glob, grep, list_directory)
PC-->>AL: ALLOWED
AL->>AL: Execute tool
else Write/execute tool
PC-->>AL: DENIED
AL->>AL: Return error to LLM
end
end
LLM-->>AL: Final response
AL-->>BE: TurnResult
end
end
BE-->>D: BootResult(executed=true, summary)
end
else bootEnabled == false
D->>D: Skip boot (log debug message)
end
D->>D: Start UDS listener
D->>D: Daemon ready
Last reviewed commit: 1aab65e |
| var executor = Executors.newVirtualThreadPerTaskExecutor(); | ||
| var future = CompletableFuture.supplyAsync(() -> { | ||
| try { | ||
| var turn = agentLoop.runTurn(bootPrompt, new ArrayList<>(), | ||
| new SilentStreamHandler()); | ||
| return turn.text(); | ||
| } catch (LlmException e) { | ||
| throw new CompletionException(e); | ||
| } | ||
| }, executor); | ||
|
|
||
| try { | ||
| return future.get(timeoutMs, TimeUnit.MILLISECONDS); | ||
| } catch (TimeoutException e) { | ||
| future.cancel(true); | ||
| throw e; | ||
| } catch (ExecutionException e) { | ||
| throw (Exception) e.getCause(); | ||
| } |
There was a problem hiding this comment.
ExecutorService created but never shut down - resource leak
| var executor = Executors.newVirtualThreadPerTaskExecutor(); | |
| var future = CompletableFuture.supplyAsync(() -> { | |
| try { | |
| var turn = agentLoop.runTurn(bootPrompt, new ArrayList<>(), | |
| new SilentStreamHandler()); | |
| return turn.text(); | |
| } catch (LlmException e) { | |
| throw new CompletionException(e); | |
| } | |
| }, executor); | |
| try { | |
| return future.get(timeoutMs, TimeUnit.MILLISECONDS); | |
| } catch (TimeoutException e) { | |
| future.cancel(true); | |
| throw e; | |
| } catch (ExecutionException e) { | |
| throw (Exception) e.getCause(); | |
| } | |
| // Run in a virtual thread with timeout | |
| var executor = Executors.newVirtualThreadPerTaskExecutor(); | |
| try { | |
| var future = CompletableFuture.supplyAsync(() -> { | |
| try { | |
| var turn = agentLoop.runTurn(bootPrompt, new ArrayList<>(), | |
| new SilentStreamHandler()); | |
| return turn.text(); | |
| } catch (LlmException e) { | |
| throw new CompletionException(e); | |
| } | |
| }, executor); | |
| try { | |
| return future.get(timeoutMs, TimeUnit.MILLISECONDS); | |
| } catch (TimeoutException e) { | |
| future.cancel(true); | |
| throw e; | |
| } catch (ExecutionException e) { | |
| throw (Exception) e.getCause(); | |
| } | |
| } finally { | |
| executor.close(); | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: aceclaw-daemon/src/main/java/dev/aceclaw/daemon/BootExecutor.java
Line: 186-204
Comment:
`ExecutorService` created but never shut down - resource leak
```suggestion
// Run in a virtual thread with timeout
var executor = Executors.newVirtualThreadPerTaskExecutor();
try {
var future = CompletableFuture.supplyAsync(() -> {
try {
var turn = agentLoop.runTurn(bootPrompt, new ArrayList<>(),
new SilentStreamHandler());
return turn.text();
} catch (LlmException e) {
throw new CompletionException(e);
}
}, executor);
try {
return future.get(timeoutMs, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
future.cancel(true);
throw e;
} catch (ExecutionException e) {
throw (Exception) e.getCause();
}
} finally {
executor.close();
}
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Fixed in 21405e8 — wrapped executor in try-finally with executor.close(). @greptileai please re-review.
There was a problem hiding this comment.
Looks good! The executor.close() in the finally block at BootExecutor.java:202 properly handles cleanup for all exit paths (success, timeout, and exceptions). Resource leak is resolved.
Wrap the virtual thread executor in try-finally to ensure it is closed after boot file execution completes, times out, or fails.
|
@greptile review |
|
@coderabbitai review |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
aceclaw-daemon/src/main/java/dev/aceclaw/daemon/BootExecutor.java (1)
49-55: Consider enforcing the documented max‑iterations guardrail.The class‑level guardrail mentions a max‑iteration limit, but it isn’t wired into the boot agent loop. Either apply it (if supported by
AgentLoopConfig) or drop the guardrail constant/docs to avoid drift.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@aceclaw-daemon/src/main/java/dev/aceclaw/daemon/BootExecutor.java` around lines 49 - 55, BootExecutor declares BOOT_MAX_ITERATIONS but never enforces it; either wire it into the boot agent loop by applying the constant to the agent loop configuration (e.g., set BOOT_MAX_ITERATIONS on the AgentLoopConfig instance used to create/start the boot agent) or enforce it directly inside the BootExecutor boot loop (increment a local counter in the method that runs the loop and break/throw when counter > BOOT_MAX_ITERATIONS). Update uses of AgentLoopConfig (or the method that starts the loop) to accept the max-iterations value, or add the explicit counter guard in the method that performs the boot agent iterations, and keep the BOOT_MAX_ITERATIONS constant (or remove it if you choose not to enforce).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@aceclaw-daemon/src/main/java/dev/aceclaw/daemon/BootExecutor.java`:
- Around line 49-55: BootExecutor declares BOOT_MAX_ITERATIONS but never
enforces it; either wire it into the boot agent loop by applying the constant to
the agent loop configuration (e.g., set BOOT_MAX_ITERATIONS on the
AgentLoopConfig instance used to create/start the boot agent) or enforce it
directly inside the BootExecutor boot loop (increment a local counter in the
method that runs the loop and break/throw when counter > BOOT_MAX_ITERATIONS).
Update uses of AgentLoopConfig (or the method that starts the loop) to accept
the max-iterations value, or add the explicit counter guard in the method that
performs the boot agent iterations, and keep the BOOT_MAX_ITERATIONS constant
(or remove it if you choose not to enforce).
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@aceclaw-daemon/src/main/java/dev/aceclaw/daemon/BootExecutor.java`:
- Around line 203-205: In the ExecutionException catch block in BootExecutor
(the code catching ExecutionException e), avoid the unsafe cast of e.getCause()
to Exception; instead retrieve Throwable cause = e.getCause() and rethrow it
appropriately: if cause is an Error, throw (Error) cause; else if cause is a
RuntimeException, throw (RuntimeException) cause; else if cause is an Exception,
throw (Exception) cause; otherwise wrap the Throwable in a new Exception (e.g.,
throw new Exception(cause)). This preserves Error types and avoids
ClassCastException while keeping the original cause.
- Around line 100-108: BOOT_MAX_ITERATIONS is defined but never used; add an
enforcement path by extending AgentLoopConfig to carry a maxIterations field
(and builder method) and set it from BOOT_MAX_ITERATIONS in BootExecutor when
constructing bootConfig, then update StreamingAgentLoop to read
config.maxIterations and stop the loop after that many iterations (or throw a
controlled exception/exit). Specifically: add a maxIterations:int (or Integer)
to the AgentLoopConfig record and builder, set
.maxIterations(BOOT_MAX_ITERATIONS) in BootExecutor where bootConfig is built,
and make StreamingAgentLoop's main run/iterate method check
AgentLoopConfig.maxIterations each iteration to enforce the guardrail.
| // Create a boot-restricted agent loop with read-only permission checker | ||
| var bootPermChecker = new BootPermissionChecker(); | ||
| var bootConfig = AgentLoopConfig.builder() | ||
| .sessionId("boot") | ||
| .permissionChecker(bootPermChecker) | ||
| .build(); | ||
| var agentLoop = new StreamingAgentLoop( | ||
| llmClient, toolRegistry, model, systemPrompt, | ||
| maxTokens, thinkingBudget, null, bootConfig); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find the BootExecutor file and check its content
find . -type f -name "BootExecutor.java" | head -5Repository: xinhuagu/AceClaw
Length of output: 127
🏁 Script executed:
# Search for BOOT_MAX_ITERATIONS definition and usage
rg -n "BOOT_MAX_ITERATIONS" --type javaRepository: xinhuagu/AceClaw
Length of output: 183
🏁 Script executed:
# Check AgentLoopConfig for maxIterations method
rg -n "maxIterations" --type javaRepository: xinhuagu/AceClaw
Length of output: 42
🏁 Script executed:
# Also check the AgentLoopConfig builder pattern
rg -n "class AgentLoopConfig" --type java -A 30Repository: xinhuagu/AceClaw
Length of output: 42
🏁 Script executed:
# Search for AgentLoopConfig definition more broadly
rg -n "AgentLoopConfig" --type java | head -20Repository: xinhuagu/AceClaw
Length of output: 2976
🏁 Script executed:
# Read the BootExecutor.java file to understand the context
cat -n aceclaw-daemon/src/main/java/dev/aceclaw/daemon/BootExecutor.java | head -110Repository: xinhuagu/AceClaw
Length of output: 5291
🏁 Script executed:
# Read the AgentLoopConfig class definition
cat -n aceclaw-core/src/main/java/dev/aceclaw/core/agent/AgentLoopConfig.javaRepository: xinhuagu/AceClaw
Length of output: 2959
🏁 Script executed:
# Check if StreamingAgentLoop or AgentLoop has any iteration limiting logic
rg -n "maxIterations\|iterations\|BOOT_MAX" --type java aceclaw-core/src/main/java/dev/aceclaw/core/agent/StreamingAgentLoop.java aceclaw-core/src/main/java/dev/aceclaw/core/agent/AgentLoop.javaRepository: xinhuagu/AceClaw
Length of output: 42
BOOT_MAX_ITERATIONS is defined but never enforced.
The constant BOOT_MAX_ITERATIONS = 10 is defined at line 50 and documented as a guardrail (line 40), but it is never used. AgentLoopConfig does not support iteration limiting—the record and builder contain only sessionId, eventBus, permissionChecker, memoryHandler, and metricsCollector. This means the max iterations guardrail documented in the Javadoc is not actually enforced during boot execution, leaving the agent loop vulnerable to runaway iterations.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@aceclaw-daemon/src/main/java/dev/aceclaw/daemon/BootExecutor.java` around
lines 100 - 108, BOOT_MAX_ITERATIONS is defined but never used; add an
enforcement path by extending AgentLoopConfig to carry a maxIterations field
(and builder method) and set it from BOOT_MAX_ITERATIONS in BootExecutor when
constructing bootConfig, then update StreamingAgentLoop to read
config.maxIterations and stop the loop after that many iterations (or throw a
controlled exception/exit). Specifically: add a maxIterations:int (or Integer)
to the AgentLoopConfig record and builder, set
.maxIterations(BOOT_MAX_ITERATIONS) in BootExecutor where bootConfig is built,
and make StreamingAgentLoop's main run/iterate method check
AgentLoopConfig.maxIterations each iteration to enforce the guardrail.
| } catch (ExecutionException e) { | ||
| throw (Exception) e.getCause(); | ||
| } |
There was a problem hiding this comment.
Unsafe cast from Throwable to Exception can mask Error types.
If the underlying task throws an Error (e.g., OutOfMemoryError, StackOverflowError), e.getCause() returns a Throwable that is not an Exception. The cast would throw ClassCastException, masking the original error.
Proposed fix to handle all Throwable types
} catch (ExecutionException e) {
- throw (Exception) e.getCause();
+ Throwable cause = e.getCause();
+ if (cause instanceof Exception ex) {
+ throw ex;
+ } else if (cause instanceof Error err) {
+ throw err;
+ } else {
+ throw new RuntimeException(cause);
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| } catch (ExecutionException e) { | |
| throw (Exception) e.getCause(); | |
| } | |
| } catch (ExecutionException e) { | |
| Throwable cause = e.getCause(); | |
| if (cause instanceof Exception ex) { | |
| throw ex; | |
| } else if (cause instanceof Error err) { | |
| throw err; | |
| } else { | |
| throw new RuntimeException(cause); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@aceclaw-daemon/src/main/java/dev/aceclaw/daemon/BootExecutor.java` around
lines 203 - 205, In the ExecutionException catch block in BootExecutor (the code
catching ExecutionException e), avoid the unsafe cast of e.getCause() to
Exception; instead retrieve Throwable cause = e.getCause() and rethrow it
appropriately: if cause is an Error, throw (Error) cause; else if cause is a
RuntimeException, throw (RuntimeException) cause; else if cause is an Exception,
throw (Exception) cause; otherwise wrap the Throwable in a new Exception (e.g.,
throw new Exception(cause)). This preserves Error types and avoids
ClassCastException while keeping the original cause.
Summary
BootExecutorthat discovers and runsBOOT.mdfiles at daemon startup for workspace initializationBOOT.mdsupports project-level (.aceclaw/BOOT.md) and global (~/.aceclaw/BOOT.md) — both execute in orderread_file,glob,grep,list_directoryauto-approved; all write/execute tools deniedbootEnabled(default true),bootTimeoutSeconds(default 120)AceClawDaemon.start()between health monitor start and UDS listener start (runs before accepting connections)Test plan
BootExecutorTest./gradlew clean build -x :aceclaw-core:test— core test failures are pre-existing)Closes #33
Summary by CodeRabbit
New Features
Tests