Skip to content

Feat/splash engine rebuild#1

Open
AzizX-coder wants to merge 3 commits into
mainfrom
feat/splash-engine-rebuild
Open

Feat/splash engine rebuild#1
AzizX-coder wants to merge 3 commits into
mainfrom
feat/splash-engine-rebuild

Conversation

@AzizX-coder

@AzizX-coder AzizX-coder commented Jun 18, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

Release Notes

  • New Features

    • Project rebranded from AlpClaw to Splash; use splash CLI instead of alpclaw.
    • Agent self-correction: improved reasoning with automated step verification and contract evolution tracking.
    • Semantic caching for faster repeated task resolution.
    • Weighted verification strategies for flexible output validation.
    • Webhook reliability: automatic retry logic with Dead Letter Queue for failed deliveries.
    • Provider cost tracking: monitor LLM usage expenses per request.
    • Enhanced TUI with trace replay and contract evolution insights.
  • Configuration Changes

    • Environment variables renamed from ALPCLAW_* to SPLASH_*.
    • Configuration paths updated from ~/.alpclaw/ to ~/.splash/.

Splash Agent added 2 commits June 11, 2026 14:41
Major multi-session work delivered as one cohesive patch:

# Rebrand
- Full @alpclaw/* -> @splash/* rename across 11 packages, AlpClaw class -> Splash,
  AlpClawError/AlpClawConfig -> SplashError/SplashConfig, ALPCLAW_* -> SPLASH_*,
  packages/core/src/alpclaw.ts -> splash.ts; removed legacy �lpclaw bin alias
  and the dead ~/.alpclaw config migration. Branding scan clean.

# Engine — all 10 unique algorithms (spec sec 1)
- Contract-first execution + DAG executor with retry/fallback/timeout/checkpoint
- Multi-strategy verifier (exact/contains/semantic/schema/function, weighted)
- Self-correction loop bounded by maxRetries
- Contract evolution (per-template trial/verified/review grading)
- Semantic response cache (cosine >= 0.92, signature-invalidated)
- Hierarchical token budget (10pct contract / 5pct verify reserves; cheap-tier downgrade
  under pressure)
- Adversarial classification -> trash memory (no auto-promote)
- Reflector learning loop with quality scoring
- Circuit-breaker provider router with cost accounting + per-task RoutingPolicy
- Deterministic replay log (.splashlog artifact, provider-free reconstruction)

# Gateway (new package @splash/gateway, zero new deps)
- HTTP API (POST/GET runs, stop/retry, /health, /metrics)
- SSE log streaming + native RFC 6455 WebSocket (/v1/ws/:id)
- Auth: SHA-256-hashed API keys (constant-time) + HS256 JWT with tenant claim
- Token-bucket rate limiting, multi-tenant run isolation
- Prometheus metrics registry; security gate refuses 0.0.0.0+auth:none
- InProcessExecutor + startGateway() wires Splash core

# Tools — 12 total (5 new this arc)
- New: CacheTool, TimerTool, AiTool, MonitorTool, QueueTool

# Skills — 43 total (4 new this arc)
- New: JsonSchemaValidatorSkill, EmailDrafterSkill, TranslatorSkill, CodeFormatSkill

# Providers (14)
- Real Nous Portal provider (replaces stub) with documented free + paid models

# CLI
- Fast-path resolver: skills/connectors/providers list+test, memory search, cache stats,
  config doctor, replay list, version, help — all sub-200ms, no LLM
- New commands: splash replay, splash gateway, splash migrate (--dry-run),
  splash stats, splash doctor (wired)
- New "ANSI Shadow" SPLASH banner with aqua wave motif

# Migration (new @splash/migrate package)
- Pure migrateHermes / migrateOpenClaw mappers (config / keys / safety / channels ->
  bots / mcp servers); minimal YAML reader; --dry-run support; applyPatch deep-merge

# Deployment
- Multi-stage distroless Dockerfile (non-root, /data/.splash volume, healthcheck script)
- Docs: ARCHITECTURE, ENGINE-INTERNALS, EXTENSION-POINTS, DEPLOYMENT, ENGINE-COMPARISON,
  MIGRATION-HERMES, MIGRATION-OPENCLAW, IMPLEMENTATION-PLAN

# Verification
- 288 unit + integration tests passing across 37 test files
- Zero TypeScript errors repo-wide
- pnpm build (tsup) succeeds
- CLI commands verified live: version, stats, doctor, skills list (52 tools/skills)
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@AzizX-coder, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 37 minutes and 46 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3a787614-ee7c-40ae-9e42-111959d9abbf

📥 Commits

Reviewing files that changed from the base of the PR and between 2edeef6 and ccc8ada.

📒 Files selected for processing (2)
  • docs/RELEASE.md
  • scripts/release.mjs
📝 Walkthrough

Walkthrough

The entire monorepo is renamed from AlpClaw to Splash (@alpclaw/*@splash/*) across all packages, environment variables, CLI binaries, and configuration. Alongside the rename, substantive new capabilities are added: circuit-breaker provider routing with cost attribution, SafetyEngine credential redaction and prompt-injection detection, AgentLoop semantic caching and bounded self-correction, weighted multi-strategy Verifier, WebhookConnector HMAC signing with retry/DLQ, and a tabbed TUI dashboard.

Changes

Splash rename and runtime feature additions

Layer / File(s) Summary
Package identity and build tooling rename
package.json, tsconfig.json, vitest.config.ts, tsup.config.ts, .env.example, bin/splash.mjs, packages/*/package.json, examples/package.json
All package names, workspace dependency keys, TypeScript path aliases, Vitest resolve aliases, tsup noExternal pattern, and environment variable prefixes are renamed from @alpclaw/* / ALPCLAW_* to @splash/* / SPLASH_*. The alpclaw bin entry and its bin/alpclaw.mjs launcher are removed. Version bumped to 2.5.1.
Core types: SplashError, SplashConfig, logger, theme
packages/utils/src/types.ts, packages/utils/src/logger.ts, packages/utils/src/theme.ts, packages/config/src/schema.ts, packages/config/src/loader.ts, packages/config/src/index.ts, packages/config/src/global-store.ts
AlpClawError is replaced by SplashError with an optional statusCode field; createError is updated accordingly. AlpClawConfig is renamed to SplashConfig and gains memory.semanticCache. CompletionResponse adds providerUsed. The logger reads only SPLASH_LOG_LEVEL; the theme banner is updated with a new wave wordmark and dual-color rendering; the animation skip gate checks SPLASH_NO_ANIM. readGlobalConfig drops the migrateFromLegacy call.
SafetyEngine: prompt injection, credential scanning, sandbox path
packages/safety/src/engine.ts, packages/safety/src/engine.test.ts, packages/safety/src/policies.ts, packages/safety/src/validator.ts
Four new public methods are added to SafetyEngine: scanPromptInjection, scanCredentials, redactCredentials (replaces matches with [REDACTED:label]), and resolveSandboxedPath. Tests cover redactCredentials across OpenAI, Slack, and Google key formats.
Provider router: circuit breaker, cost attribution, RoutingPolicy, NousProvider
packages/providers/src/router.ts, packages/providers/src/router.test.ts, packages/providers/src/index.ts, packages/providers/src/nous.ts, packages/providers/src/openai.ts, packages/providers/src/...
ProviderRouter adds per-provider circuit breakers with exponential backoff, error logging to ~/.splash/logs/error.jsonl, and costUsd step attribution computed from static COST_RATES by costTier. setRoutingPolicy installs a RoutingPolicy consulted before score-based selection. NousProvider is reimplemented as a real OpenAIProvider subclass with a static model catalog. OpenAI.complete() adds statusCode to errors. New router tests cover circuit breaker, cost, and policy selection.
WebhookConnector: HMAC signing, retry/backoff, DLQ
packages/connectors/src/webhook.ts
registerEndpoint and the constructor accept hmacSecret and maxRetries. sendPost computes X-Hub-Signature-256, retries with exponential backoff, and writes failed payloads to a local DLQ file on final failure. A new replayDlq action re-sends queued entries and rewrites the file with only remaining failures.
Verifier: multi-strategy weighted verification
packages/core/src/verifier.ts, packages/core/src/verifier.test.ts
New exports VerifierStrategy, WeightedCriterion, CriterionResult, and WeightedVerification. verifyWeighted computes a weighted 0–1 score across criteria using evaluateStrategy, which implements exact/contains, Jaccard semantic similarity, minimal JSON-schema validation, and custom function predicates. Tests cover all strategy types, partial-weight scoring, and empty-criteria behavior.
AgentLoop: semantic cache, self-correction, credential redaction, replay/evolution
packages/core/src/agent-loop.ts, packages/core/src/task-manager.ts, packages/core/src/self-corrector.ts, packages/core/src/planner.ts, packages/core/src/runs/..., packages/core/src/index.ts
AgentLoop conditionally initializes SemanticCache and adds a semantic lookup after exact cache miss. Single-pass execution is replaced by a bounded self-correction loop (verify → build correction contract → re-execute → merge). finalSummary is credential-redacted before caching. Replay log and contract evolution are recorded as guarded side effects. TaskManager.addStep gains optional stepId; addSteps is added for bulk creation. Core index removes AlpClaw export and adds SemanticCache, FastPath, ContractEvolution, correction helpers, and gateway exports.
TUI: tabbed dashboard with MemoryPane, ContractPane, trace replay
packages/core/src/tui/app.tsx
TuiApp is refactored into a tabbed Ink UI (task graph / memory / contracts). MemoryPane periodically polls MemoryManager/Reflector and renders quality bars and session data. ContractPane polls ContractEvolution templates with status icons and success rates. LogsPane supports live and trace-replay modes with cursor highlighting. ResourcesPane shows steps/toolCalls/retries and switches to ThinkingAnimation when running.
CLI: fast-path dispatch, new commands, Splash runtime wiring
examples/cli.ts, examples/demo-standalone.ts
All imports migrate to @splash/*. buildAgent/runOneShot/printStatusLine use Splash.create(). dispatchFastPath executes FastPathCommand without running the full agent. New commands gateway, migrate, replay, and stats are added. The init wizard gains a dynamic PluginManager import. Swarm, self-improvement, and self-modification paths use Splash runtime. Bot execution uses SPLASH_HOME. Plugins subcommands shift to list/add/remove.
Bot connectors, skill/connector imports, and tests
bots/..., packages/connectors/src/..., packages/memory/src/..., packages/skills/src/..., tests/integration.test.ts
All bot connectors swap getAlpClawgetSplash and update boot banners. bots/lib/chat-agent.ts exports getSplash(). SubagentRunnerSkill dynamically imports @splash/core. All connector, memory, and skill source files swap @alpclaw/* imports to @splash/*. Sandbox directory names change to splash-sandbox. Integration and unit tests update package references and .splash/ filesystem paths. Four new skills are exported from the skills index.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as examples/cli.ts
  participant Splash as Splash.create()
  participant Router as ProviderRouter
  participant CircuitBreaker
  participant AgentLoop
  participant SemanticCache
  participant Safety as SafetyEngine

  CLI->>Splash: buildAgent()
  Splash-->>CLI: splash instance
  CLI->>AgentLoop: splash.createAgent(callbacks)
  AgentLoop->>SemanticCache: semantic lookup(task)
  alt semantic hit
    SemanticCache-->>AgentLoop: cached result
  else miss
    AgentLoop->>Router: route(request, preferProvider)
    Router->>CircuitBreaker: breakerAllows(provider)?
    CircuitBreaker-->>Router: allowed
    Router-->>AgentLoop: CompletionResponse(providerUsed, costUsd)
    AgentLoop->>AgentLoop: self-correction loop (verify → correct → merge)
    AgentLoop->>Safety: redactCredentials(finalSummary)
    AgentLoop->>SemanticCache: write entry
  end
  AgentLoop-->>CLI: ChatRunResult
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 A splash, not a claw, shall be our new name,
The packages renamed, the circuits retamed,
With HMAC webhooks and DLQs in store,
Weighted Verifiers at every door,
The TUI now tabs — memory, contract, and trace,
@splash/* at last has taken its place! 🌊

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main objective: rebranding/rebuilding the codebase from AlpClaw to Splash, which is the primary change across this extensive changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/splash-engine-rebuild

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
examples/cli.ts (1)

2069-2074: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Redundant path checks: splashChar and globalChar are identical.

Both variables resolve to ~/.splash/character.md, making the second existsSync check unreachable.

🛠️ Suggested fix
   const splashChar = path.resolve(home, ".splash", "character.md");
-  const globalChar = path.resolve(home, ".splash", "character.md");
   if (fs.existsSync(localChar)) return fs.readFileSync(localChar, "utf-8");
   if (fs.existsSync(splashChar)) return fs.readFileSync(splashChar, "utf-8");
-  if (fs.existsSync(globalChar)) return fs.readFileSync(globalChar, "utf-8");
   return undefined;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/cli.ts` around lines 2069 - 2074, The variables splashChar and
globalChar both resolve to the identical path path.resolve(home, ".splash",
"character.md"), making the second existsSync check for globalChar unreachable.
Update the globalChar variable to resolve to a different path location, likely
in the home directory directly (such as path.resolve(home, "character.md")) to
provide the intended fallback hierarchy of checking local, then splash
subdirectory, then global home directory locations.
packages/skills/src/built-in/web-search.ts (1)

36-39: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add a timeout for the external search request.

Line 36 issues an external HTTP request without a timeout, so slow upstream responses can block the skill indefinitely.

⏱️ Suggested fix
-      const res = await fetch(url);
+      const controller = new AbortController();
+      const timeout = setTimeout(() => controller.abort(), 10_000);
+      let res: Response;
+      try {
+        res = await fetch(url, { signal: controller.signal });
+      } finally {
+        clearTimeout(timeout);
+      }
       if (!res.ok) {
         return err(createError("skill", `Search execution failed: HTTP ${res.status}`));
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/skills/src/built-in/web-search.ts` around lines 36 - 39, The fetch
request to the external search URL lacks a timeout mechanism, which could cause
the skill to hang indefinitely if the upstream response is slow. Create an
AbortController with a timeout duration, pass the signal to the fetch call, and
ensure any AbortError is caught and converted to an appropriate error response.
This will prevent the external HTTP request from blocking the skill
indefinitely.
packages/skills/src/built-in/web-scraper.ts (1)

28-40: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Block SSRF targets before fetching user-provided URLs.

Line 28 accepts untrusted URLs and Line 34 fetches them directly. This permits requests to localhost/private/link-local/metadata endpoints from the runtime. Add protocol + hostname/IP validation before fetch.

🔒 Suggested hardening
   async execute(params: Record<string, unknown>, ctx: SkillContext): Promise<Result<SkillResult>> {
-    const url = String(params.url || "");
+    const url = String(params.url || "").trim();
     const question = params.question ? String(params.question) : null;
     if (!url) return err(createError("validation", "web-scraper requires url"));

     try {
+      const target = new URL(url);
+      if (!["http:", "https:"].includes(target.protocol)) {
+        return err(createError("validation", "Only http/https URLs are allowed"));
+      }
+      await assertPublicHost(target.hostname); // block localhost/private/link-local/metadata
+
       ctx.log(`Fetching URL: ${url}`);
-      const res = await fetch(url, {
+      const res = await fetch(target.toString(), {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/skills/src/built-in/web-scraper.ts` around lines 28 - 40, The
web-scraper skill accepts untrusted user-provided URLs via params.url and
directly fetches them without validation, creating a Server-Side Request Forgery
(SSRF) vulnerability that allows requests to localhost, private IP ranges,
link-local addresses, and metadata endpoints. Before the fetch call, add URL
validation that checks the protocol (whitelist http/https only), parses the
hostname/IP, and blocks requests to localhost, 127.0.0.1, private IP ranges
(10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), link-local addresses
(169.254.0.0/16), and metadata endpoints. Return a validation error if any of
these checks fail before attempting to fetch the URL.
packages/connectors/src/webhook.ts (1)

101-120: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

replayDlq action always fails due to endpoint validation.

The endpoint lookup at lines 102-108 runs unconditionally before the switch. Since replayDlq doesn't require an endpoint parameter, alias is undefined, causing immediate failure with "Unknown webhook endpoint: undefined".

Move the endpoint validation inside the send and fetch cases, or handle replayDlq before the validation block.

Proposed fix
 async execute(action: string, args: Record<string, unknown>): Promise<Result<unknown>> {
+   if (action === "replayDlq") {
+     return this.replayDlq();
+   }
+
    const alias = args["endpoint"] as string;
    const endpoint = this.endpoints.get(alias);
    if (!endpoint) {
      return err(
        createError("connector", `Unknown webhook endpoint: ${alias}. Register it first.`),
      );
    }

    switch (action) {
      case "send":
        return this.sendPost(alias, endpoint, args["payload"] as Record<string, unknown>);
      case "fetch":
        return this.sendGet(endpoint, args["queryParams"] as Record<string, string> | undefined);
-     case "replayDlq":
-       return this.replayDlq();
      default:
        return err(createError("connector", `Unknown webhook action: ${action}`));
    }
  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/connectors/src/webhook.ts` around lines 101 - 120, The endpoint
validation block (the if statement checking if !endpoint exists) runs
unconditionally before the switch statement in the execute method, but the
replayDlq action does not require an endpoint parameter. This causes the
validation to fail when alias is undefined. Move the endpoint validation logic
inside only the send and fetch cases within the switch statement where endpoints
are actually needed, allowing replayDlq to execute without requiring endpoint
lookup. The sendPost and sendGet calls should perform their own endpoint
validation after being moved inside their respective cases.
🟠 Major comments (18)
packages/core/src/tui/app.tsx-314-319 (1)

314-319: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Input handler conflict: MemoryPane's useInput fires alongside TuiApp's.

Both MemoryPane and TuiApp register useInput handlers for the same keys (↑/↓, j/k). In Ink, all registered handlers receive every keystroke, so navigating runs in live mode will also move the MemoryPane cursor (and vice versa when the Memory tab is active). Consider guarding the handler with a check for the active tab or lifting cursor state to the parent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/tui/app.tsx` around lines 314 - 319, The useInput handler
in the TuiApp component (which handles the upArrow, downArrow, j, and k keys to
move the cursor via setCursor) is firing even when the MemoryPane tab is active,
causing both components to respond to the same keypresses simultaneously. Guard
the handler logic by checking if the current active tab is not the MemoryPane
tab before executing the setCursor calls, so that keyboard navigation only works
for the currently active tab. This will prevent input conflicts between TuiApp's
cursor navigation and MemoryPane's own input handling.
packages/safety/src/engine.ts-175-176 (1)

175-176: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Slack token regex only redacts/detects a token prefix, not full canonical tokens.

The current pattern stops at the first hyphenated segment. For typical Slack tokens (xoxb-...-...-...), trailing segments can remain unredacted/unreported.

🛡️ Proposed fix
-      /xox[baprs]-[a-zA-Z0-9]{10,}/, // Slack
+      /\bxox[baprs]-[A-Za-z0-9]{8,}(?:-[A-Za-z0-9]{4,})+\b/, // Slack (multi-segment)

-      { re: /xox[baprs]-[a-zA-Z0-9]{10,}/g, label: "slack-token" },
+      { re: /\bxox[baprs]-[A-Za-z0-9]{8,}(?:-[A-Za-z0-9]{4,})+\b/g, label: "slack-token" },

Also applies to: 200-201

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/safety/src/engine.ts` around lines 175 - 176, The Slack token regex
pattern at the specified location only matches partial tokens by stopping after
the first hyphenated segment. Update the regex pattern to match the complete
Slack token format which includes multiple hyphenated segments
(xoxb-...-...-...). The pattern should be modified to account for the full token
structure with additional hyphen-separated alphanumeric components. Also apply
the same fix to the similar pattern mentioned at lines 200-201.
package.json-3-3 (1)

3-3: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Breaking CLI contract needs a major version bump (or compatibility alias).

Line 37 removes the alpclaw bin, which is a breaking CLI change for existing users, but Line 3 only bumps patch version. Please either restore a compatibility alias for one transition release or bump to a major version.

Suggested fix (major bump path)
-  "version": "2.5.1",
+  "version": "3.0.0",

Also applies to: 37-37

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 3, The removal of the alpclaw bin entry represents a
breaking change to the CLI contract, which requires a major version bump rather
than the current patch version bump. Update the version field in package.json
from the current patch version (2.5.1) to a major version (3.0.0) to properly
communicate this breaking change to users and follow semantic versioning
conventions.
packages/config/src/global-store.ts-9-9 (1)

9-9: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restore real legacy path + first-read migration to avoid losing existing config.

Line 45 points legacyConfigDir() to .splash, so it can never target prior ~/.alpclaw data. Combined with Lines 60-67 (no migration/fallback read), existing users’ legacy config (including API keys) is silently ignored.

Suggested fix
 export function legacyConfigDir(): string {
-  return path.join(os.homedir(), ".splash");
+  return path.join(os.homedir(), ".alpclaw");
 }

 export function readGlobalConfig(): GlobalConfigShape {
-  const p = globalConfigPath();
-  if (!fs.existsSync(p)) return {};
+  const p = globalConfigPath();
+  if (!fs.existsSync(p)) {
+    const legacyPath = path.join(legacyConfigDir(), "config.json");
+    if (fs.existsSync(legacyPath)) {
+      ensureGlobalConfigDir();
+      fs.copyFileSync(legacyPath, p);
+    } else {
+      return {};
+    }
+  }
   try {
     return JSON.parse(fs.readFileSync(p, "utf-8")) as GlobalConfigShape;
   } catch {
     return {};
   }
 }

Also applies to: 45-45, 60-67

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/config/src/global-store.ts` at line 9, The legacyConfigDir()
function at line 45 is pointing to the wrong directory path and the config
reading logic at lines 60-67 lacks fallback/migration logic. To fix this, update
the legacyConfigDir() function to return the actual prior legacy path (the real
~/.alpclaw directory) instead of .splash, and then add migration/fallback logic
in the config reading section that attempts to read from the legacy config
directory first, migrating it to the new location if found, before defaulting to
the new location. This ensures existing users' configurations including API keys
are properly migrated and not silently lost.
bots/lib/chat-agent.ts-12-13 (1)

12-13: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make Splash singleton initialization atomic.

The current lazy init can race under concurrent calls and create multiple Splash.create() instances before _splash is assigned.

🔧 Suggested change
 let _splash: Splash | null = null;
+let _splashInit: Promise<Splash> | null = null;
 let _personaCache: string | undefined = undefined;
@@
 export async function getSplash(): Promise<Splash> {
-  if (!_splash) _splash = await Splash.create();
-  return _splash;
+  if (_splash) return _splash;
+  if (!_splashInit) {
+    _splashInit = Splash.create()
+      .then((instance) => {
+        _splash = instance;
+        return instance;
+      })
+      .finally(() => {
+        _splashInit = null;
+      });
+  }
+  return _splashInit;
 }

Also applies to: 30-32

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bots/lib/chat-agent.ts` around lines 12 - 13, The lazy initialization of the
_splash singleton variable is not atomic and can race under concurrent calls,
causing multiple Splash.create() instances to be created. To fix this, implement
a Promise-based initialization pattern for both _splash and _personaCache (as
mentioned in lines 30-32) where you store the initialization promise alongside
the value, and ensure that subsequent concurrent calls await the same
initialization promise rather than triggering new create() calls. This ensures
only one instance is ever created regardless of concurrent access patterns.
bots/slack.ts-79-80 (1)

79-80: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Await Splash bootstrap in startup path.

Fire-and-forget initialization can log readiness too early and mask bootstrap failure handling.

🔧 Suggested change
-  getSplash(); // warm up the agent platform
+  await getSplash(); // warm up the agent platform
   console.log(pc.green("✓ Framework initialized."));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bots/slack.ts` around lines 79 - 80, The getSplash() function call is not
being awaited, which causes the console.log statement to execute before the
agent platform bootstrap is complete. Add the await keyword before getSplash()
to ensure the initialization completes before logging the success message and
continuing with the startup sequence, allowing proper error handling if the
bootstrap fails.
bots/messenger.ts-66-67 (1)

66-67: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Await framework warm-up before announcing initialization.

getSplash() should be awaited here to prevent false-ready logs and startup rejection escaping.

🔧 Suggested change
-  getSplash();
+  await getSplash();
   console.log(pc.green("✓ Framework initialized."));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bots/messenger.ts` around lines 66 - 67, The getSplash() function call is not
being awaited, which allows the framework initialization success message to be
logged before the framework warm-up completes. Add the await keyword before the
getSplash() call to ensure the asynchronous operation completes before the
success log message is printed, preventing false-ready logs and ensuring startup
rejections are properly handled.
bots/whatsapp.ts-87-88 (1)

87-88: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Await Splash initialization in main startup flow.

Without awaiting, startup can claim initialization success before framework creation completes.

🔧 Suggested change
-  getSplash();
+  await getSplash();
   console.log(pc.green("✓ Framework initialized."));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bots/whatsapp.ts` around lines 87 - 88, The getSplash() function call in the
main startup flow is not being awaited, which allows the initialization success
message to be logged before the splash initialization actually completes. Add
the await keyword before the getSplash() call to ensure it finishes before the
console.log statement executes and reports that the framework is initialized.
bots/telegram.ts-39-40 (1)

39-40: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Await connector bootstrap before creating the bot runtime.

Initialization is currently fire-and-forget; awaiting avoids false-ready startup and unhandled bootstrap rejection paths.

🔧 Suggested change
-  getSplash();
+  await getSplash();
   const bot = new Telegraf(token);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bots/telegram.ts` around lines 39 - 40, Add an await keyword to the
getSplash() function call to ensure the connector bootstrap completes before the
Telegraf bot instance is created. The getSplash() call should be awaited on the
line before the new Telegraf(token) instantiation to guarantee proper
initialization sequencing and rejection handling.
bots/discord.ts-80-81 (1)

80-81: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Await Splash bootstrap before logging framework readiness.

getSplash() is async; fire-and-forget startup can report success before initialization finishes and can drop bootstrap failures into unhandled rejection behavior.

🔧 Suggested change
-  getSplash();
+  await getSplash();
   console.log(pc.green("✓ Framework initialized."));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bots/discord.ts` around lines 80 - 81, The getSplash() function is
asynchronous but is being called without awaiting it, causing the framework
readiness log message to print before the splash initialization actually
completes. This creates a race condition where the success message can be logged
before startup finishes, and any errors in getSplash() become unhandled
rejections. Add the await keyword before the getSplash() call to ensure it
completes before logging the success message with pc.green().
packages/core/src/verifier.ts-175-177 (1)

175-177: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle non-serializable outputs safely in strategy evaluation.

JSON.stringify(output) can throw on circular structures, which turns verification into a runtime failure.

Suggested fix
-    const text = typeof output === "string" ? output : JSON.stringify(output ?? "");
+    let text: string;
+    if (typeof output === "string") {
+      text = output;
+    } else {
+      try {
+        text = JSON.stringify(output ?? "");
+      } catch {
+        text = String(output);
+      }
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/verifier.ts` around lines 175 - 177, The evaluateStrategy
method calls JSON.stringify on the output parameter without error handling,
which can throw when the output contains circular structures or non-serializable
objects, causing the verification to fail with a runtime error. Wrap the
JSON.stringify call in a try-catch block to safely handle serialization
failures, and provide a fallback string representation (such as "output") when
serialization throws an error instead of propagating the exception.
packages/core/src/verifier.ts-160-168 (1)

160-168: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject non-positive weights to preserve the 0..1 score contract.

With weight <= 0, scoring can become inconsistent (including impossible ratios) while passed is computed independently.

Suggested fix
     for (const criterion of criteria) {
       const weight = criterion.weight ?? 1;
+      if (!Number.isFinite(weight) || weight <= 0) {
+        results.push({
+          name: criterion.name,
+          passed: false,
+          weight: 0,
+          detail: "invalid weight: must be a finite number > 0",
+        });
+        continue;
+      }
       totalWeight += weight;
       const { passed, detail } = this.evaluateStrategy(criterion.strategy, output);
       if (passed) passedWeight += weight;
       results.push({ name: criterion.name, passed, weight, detail });
     }

-    const score = totalWeight > 0 ? passedWeight / totalWeight : 1;
+    const score = totalWeight > 0 ? passedWeight / totalWeight : 0;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/verifier.ts` around lines 160 - 168, Add validation in the
results loop to reject non-positive weights and preserve the 0..1 score
contract. When processing each criterion, check that criterion.weight is
positive (greater than 0) before including it in the weight calculations for
totalWeight and passedWeight. Skip or throw an error for any criterion with
weight <= 0 to ensure the score calculation remains consistent and produces
values within the 0..1 range.
packages/core/src/verifier.ts-92-97 (1)

92-97: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

schema strategy currently passes any object without enforcing the schema.

The branch only checks “is object” and ignores strategyOptions.schema, which can incorrectly mark invalid outputs as verified.

Suggested fix
-      } else if (strategy === "schema") {
-        if (typeof output !== "object" || output === null) {
-          issues.push("Schema verification failed: output is not an object");
-        }
-        // Basic schema check could be done here based on strategyOptions.schema
+      } else if (strategy === "schema") {
+        const schema = strategyOptions?.schema as Record<string, unknown> | undefined;
+        if (!schema) {
+          issues.push("Schema verification requires strategyOptions.schema");
+        } else {
+          const r = this.validateSchema(schema, output);
+          if (!r.passed) issues.push(`Schema verification failed: ${r.detail}`);
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/verifier.ts` around lines 92 - 97, In the schema strategy
branch where strategy === "schema", the current implementation only checks if
the output is an object but completely ignores the actual schema validation
against strategyOptions.schema. You need to implement proper schema validation
by checking if the output structure matches the schema requirements defined in
strategyOptions.schema, and if validation fails, push descriptive error messages
to the issues array that indicate which parts of the schema validation failed.
Replace the placeholder comment about basic schema check with actual validation
logic that uses the schema from strategyOptions.
packages/core/src/task-manager.ts-51-58 (1)

51-58: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate supplied step IDs and fix empty-batch return behavior.

Line 57 / Line 74 accept caller IDs without duplicate checks, and Line 78 uses slice(-steps.length) which returns the full step list when steps.length === 0. This can break updateStep(...) ID targeting and return incorrect results to callers.

Suggested patch
   addStep(taskId: string, description: string, toolName?: string, stepId?: string): TaskStep {
     const task = this.tasks.get(taskId);
     if (!task) throw new Error(`Task not found: ${taskId}`);
+    if (stepId && task.steps.some((s) => s.id === stepId)) {
+      throw new Error(`Duplicate step id: ${stepId}`);
+    }

     const step: TaskStep = {
       id: stepId || generateId("step"),
       description,
       status: "pending",
       toolName,
     };
@@
   addSteps(taskId: string, steps: Array<{ id: string; description: string; toolName?: string }>): TaskStep[] {
     const task = this.tasks.get(taskId);
     if (!task) throw new Error(`Task not found: ${taskId}`);
+    if (steps.length === 0) return [];
+
+    const existing = new Set(task.steps.map((s) => s.id));
+    const incoming = new Set<string>();
+    for (const s of steps) {
+      if (!s.id) throw new Error("Step id is required");
+      if (existing.has(s.id) || incoming.has(s.id)) {
+        throw new Error(`Duplicate step id: ${s.id}`);
+      }
+      incoming.add(s.id);
+    }

     for (const s of steps) {
       const step: TaskStep = { id: s.id, description: s.description, status: "pending", toolName: s.toolName };
       task.steps.push(step);
     }
     task.updatedAt = Date.now();
     return task.steps.slice(-steps.length);
   }

Also applies to: 69-79

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/task-manager.ts` around lines 51 - 58, The addStep method
accepts an optional stepId parameter without validating that it is unique within
the task, which can cause duplicate IDs and break updateStep targeting. Add a
duplicate check in the addStep method to verify that any supplied stepId does
not already exist in the task's steps array before creating the new step.
Additionally, there is a bug in the step return logic around line 78 where
slice(-steps.length) fails to handle the edge case when steps.length equals
zero, causing incorrect results to be returned to callers. Fix this by adding
proper handling for the empty steps case before applying the slice operation.
packages/core/src/runs/run-manager.ts-198-203 (1)

198-203: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Background launcher resolution is tied to home/storage path, which can disable true detached runs.

Line 200 uses SPLASH_HOME (duplicated in the OR chain) to discover bin/splash.mjs. SPLASH_HOME is typically a data directory, so this can miss the launcher and silently fall back to inline execution.

Suggested patch
   private spawnBackground(id: string, task: string): void {
     // Find bin/splash.mjs launcher so we can re-exec ourselves with a detached child.
-    const splashHome = process.env.SPLASH_HOME || process.env.SPLASH_HOME || process.cwd();
-    const launcher = path.join(splashHome, "bin", "splash.mjs");
+    const launcher =
+      process.argv[1] && fs.existsSync(process.argv[1])
+        ? process.argv[1]
+        : path.join(process.cwd(), "bin", "splash.mjs");

     if (!fs.existsSync(launcher)) {
       // Fall back: run inline synchronously but don't block caller — fire and forget.
       void this.runInline(id, task);
       return;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/runs/run-manager.ts` around lines 198 - 203, The
spawnBackground method has a duplicate SPLASH_HOME check in the environment
variable fallback chain and incorrectly relies on SPLASH_HOME (a data directory)
to locate the bin/splash.mjs launcher, which causes the launcher to be missed
and falls back to inline execution. Remove the duplicate SPLASH_HOME reference
in the OR chain and instead resolve the launcher path relative to the
application installation location using a reliable path like __dirname or the
package installation directory, ensuring the launcher is always found for true
detached background execution.
packages/core/src/agent-loop.ts-334-352 (1)

334-352: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Redact replay-log payloads before disk persistence.

Line 345 and Line 346 write raw step output/error into replay logs. That bypasses the summary redaction path and can persist credentials/PII to local storage.

Suggested patch
       this.replayLog.record({
         runId: task.id,
         task: taskDescription,
         objective: contract.objective,
         contract,
         steps: execResult.stepResults.map((s) => ({
           id: s.stepId,
           tool: s.tool,
           success: s.success,
-          output: String(s.output ?? ""),
-          error: s.error,
+          output: this.safety.redactCredentials(String(s.output ?? "")),
+          error: s.error ? this.safety.redactCredentials(s.error) : undefined,
           durationMs: s.durationMs,
         })),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/agent-loop.ts` around lines 334 - 352, The
replayLog.record() call writes raw, unredacted step output and error data to
disk persistence, which can expose credentials and PII. In the steps array
mapping within replayLog.record(), replace the raw values for the output and
error fields with their redacted/summarized equivalents. Apply the same
redaction logic that is used in the summary redaction path to both the output
field (currently String(s.output ?? "")) and the error field (currently s.error)
to ensure sensitive data is not persisted to local storage.
packages/connectors/src/webhook.ts-180-197 (1)

180-197: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Ensure .splash directory exists before writing DLQ file.

If ~/.splash doesn't exist (e.g., fresh install), fs.writeFile will fail with ENOENT. Create the directory first.

Proposed fix
  private async addToDlq(alias: string, payload: Record<string, unknown>) {
    try {
      const fs = await import("node:fs/promises");
      const os = await import("node:os");
      const path = await import("node:path");
-     const dlqPath = path.join(os.homedir(), ".splash", "webhook_dlq.json");
+     const splashDir = path.join(os.homedir(), ".splash");
+     await fs.mkdir(splashDir, { recursive: true });
+     const dlqPath = path.join(splashDir, "webhook_dlq.json");
      let dlq: any[] = [];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/connectors/src/webhook.ts` around lines 180 - 197, The addToDlq
method writes to a file in ~/.splash/webhook_dlq.json without ensuring the
directory exists first, which causes fs.writeFile to fail with ENOENT on fresh
installations. Before calling fs.writeFile on dlqPath, create the directory
using fs.mkdir with the recursive option set to true to create any parent
directories needed. This ensures the ~/.splash directory exists before
attempting to write the webhook_dlq.json file.
packages/connectors/src/webhook.ts-155-177 (1)

155-177: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

4xx client errors are incorrectly retried.

When res.ok is false with status < 500 (e.g., 400, 401, 403, 404):

  1. Line 155 condition fails (not 5xx)
  2. Line 165 throws an error
  3. Caught at line 167, which retries if attempt < maxRetries

Client errors won't resolve with retries; this wastes time and delays the failure path. Also, line 177 is unreachable dead code since all loop paths exit via return.

Proposed fix
        if (!res.ok && res.status >= 500 && attempt < maxRetries) {
          attempt++;
          await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt))); // Exponential backoff
          continue;
        }

        const text = await res.text();
        let body: unknown;
        try { body = JSON.parse(text); } catch { body = text; }

-       if (!res.ok) throw new Error(`HTTP ${res.status}: ${text.slice(0, 100)}`);
+       if (!res.ok) {
+         await this.addToDlq(alias, payload);
+         return err(createError("connector", `Webhook POST failed: HTTP ${res.status}`, { retryable: res.status >= 500 }));
+       }
        return ok({ status: res.status, body });
      } catch (cause) {
        if (attempt < maxRetries) {
          attempt++;
          await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt)));
          continue;
        }
        await this.addToDlq(alias, payload);
        return err(createError("connector", "Webhook POST failed, added to DLQ", { cause, retryable: false }));
      }
    }
-   return err(createError("connector", "Webhook POST exhausted retries"));
  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/connectors/src/webhook.ts` around lines 155 - 177, The catch block
starting at line 167 retries all errors when attempt < maxRetries, but 4xx
client errors should not be retried since they won't resolve with retries.
Modify the error handling in the catch block to distinguish between retryable
errors (5xx or transient network errors) and non-retryable errors (4xx client
errors). For 4xx errors, immediately add to DLQ without retrying; for other
errors, apply the existing retry logic. Additionally, line 177 containing the
final error return is unreachable dead code since all code paths through the
loop exit via return statements, so this line can be removed.
🟡 Minor comments (9)
examples/cli.ts-1936-1937 (1)

1936-1937: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Suspicious path resolution: promptsDir goes two levels above ~/.splash.

path.resolve(splashDir, "..", "..", "prompts") resolves to ~/prompts (two levels up from ~/.splash). This is likely unintended and may have been meant to be path.join(splashDir, "prompts") or left over from a different directory structure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/cli.ts` around lines 1936 - 1937, The path resolution for the
promptsDir variable is using an unintended two-level directory traversal with
`path.resolve(splashDir, "..", "..", "prompts")` which results in going up two
levels from the splash directory instead of staying within it. Fix this by
replacing the path.resolve call with `path.join(splashDir, "prompts")` to
correctly resolve to the prompts subdirectory within the splash directory
structure.
examples/cli.ts-2018-2018 (1)

2018-2018: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Duplicate fallback: SPLASH_HOME || SPLASH_HOME.

The fallback chain reads the same env var twice. This was likely a copy-paste issue from ALPCLAW_HOME || SPLASH_HOME migration.

🛠️ Suggested fix
-  const splashHome = process.env.SPLASH_HOME || process.env.SPLASH_HOME || process.cwd();
+  const splashHome = process.env.SPLASH_HOME || process.cwd();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/cli.ts` at line 2018, The splashHome variable assignment contains a
duplicate fallback chain where process.env.SPLASH_HOME is checked twice in a row
before falling back to process.cwd(). Remove the redundant second
process.env.SPLASH_HOME check so the fallback chain only evaluates the
environment variable once before defaulting to process.cwd().
examples/cli.ts-821-823 (1)

821-823: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Unused variable: a from buildAgent() is never used.

The agent is created but never referenced. This wastes initialization time and resources. If not needed, remove it.

🛠️ Suggested fix
   const { TuiApp, RunManager } = await import("`@splash/core`");
-  const a = await buildAgent();
   const manager = new RunManager();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/cli.ts` around lines 821 - 823, The variable a assigned from the
buildAgent() function call is never used in the subsequent code, wasting
initialization resources. Remove the line const a = await buildAgent(); entirely
since the agent instance is not referenced anywhere after creation.
examples/cli.ts-228-230 (1)

228-230: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Unreachable code: doctor case is handled before the switch.

Lines 137-140 already handle cmd === "doctor" and return early, so this switch case at lines 228-230 will never execute.

🛠️ Suggested fix
-    case "doctor":
-      await runDoctor(args.slice(1));
-      return;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/cli.ts` around lines 228 - 230, The switch statement contains a
"doctor" case that calls runDoctor, but this case is unreachable because the
same command is already handled with an early return before the switch
statement. Remove the "doctor" case from the switch block entirely since the
early check at lines 137-140 (which handles cmd === "doctor" and returns) makes
this switch case dead code.
packages/safety/src/engine.test.ts-132-133 (1)

132-133: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid embedding secret-shaped literals directly in test source.

This literal is flagged by Betterleaks as a generic API key. Even when fake, it can create CI/security-noise and triage overhead.

🧹 Proposed tweak
-  it("redacts a Slack token", () => {
-    const out = engine.redactCredentials("token=xoxb-1234567890-abcdef");
+  it("redacts a Slack token", () => {
+    const token = ["xoxb", "1234567890", "abcdef"].join("-");
+    const out = engine.redactCredentials(`token=${token}`);
     expect(out).toContain("[REDACTED:slack-token]");
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/safety/src/engine.test.ts` around lines 132 - 133, The test in the
redactCredentials test case embeds a secret-shaped literal that resembles a real
Slack token format and is being flagged by security scanning tools. Replace the
fake token string in the engine.redactCredentials call with a generic
placeholder that does not match real API key patterns (such as common prefixes
like xoxb-, xoxp-, etc.), while ensuring the test still validates that the
redaction mechanism works correctly for the test input.

Source: Linters/SAST tools

packages/safety/src/engine.ts-179-184 (1)

179-184: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

scanCredentials returns at most one match per pattern.

Using text.match(...) once per regex drops additional credentials of the same type, so the matches array is incomplete.

✅ Proposed fix
   const matches: string[] = [];
   for (const pattern of credentialPatterns) {
-    const match = text.match(pattern);
-    if (match) {
-      matches.push(match[0]);
-    }
+    const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
+    const globalPattern = new RegExp(pattern.source, flags);
+    for (const m of text.matchAll(globalPattern)) {
+      matches.push(m[0]);
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/safety/src/engine.ts` around lines 179 - 184, The
`text.match(pattern)` call in the scanCredentials function only returns the
first match for each pattern, causing the matches array to be incomplete when
multiple credentials of the same type exist. Replace the single match call with
text.matchAll(pattern) to capture all matches for each pattern, then iterate
through all matched results (not just the first one at index [0]) to add all
credentials to the matches array.
packages/core/src/index.ts-5-5 (1)

5-5: ⚠️ Potential issue | 🟡 Minor

Add CriterionResult to the public API exports in index.ts.

CriterionResult is exported from packages/core/src/verifier.ts but not re-exported from packages/core/src/index.ts, creating an inconsistency. Since related types like WeightedCriterion and WeightedVerification are already re-exported, CriterionResult should be included for API completeness.

Update line 5 of packages/core/src/index.ts:

export { Verifier, type VerificationResult, type VerifierStrategy, type CriterionResult, type WeightedCriterion, type WeightedVerification } from "./verifier.js";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/index.ts` at line 5, The CriterionResult type is missing
from the public API exports in index.ts. Add type CriterionResult to the export
statement on line 5 where Verifier, VerificationResult, VerifierStrategy,
WeightedCriterion, and WeightedVerification are being exported from
./verifier.js. This ensures all related types from the verifier module are
consistently available in the public API.
packages/connectors/src/webhook.ts-131-131 (1)

131-131: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Same nullish coalescing issue.

Consistent with line 37, use ?? instead of || to allow explicit maxRetries: 0.

Proposed fix
-   const maxRetries = endpoint.maxRetries || 3;
+   const maxRetries = endpoint.maxRetries ?? 3;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/connectors/src/webhook.ts` at line 131, The maxRetries assignment in
the webhook.ts file uses the OR operator (`||`) instead of the nullish
coalescing operator (`??`). Replace `||` with `??` in the line `const maxRetries
= endpoint.maxRetries || 3;` to properly handle the case where maxRetries is
explicitly set to 0, which should be allowed as a valid value but would
currently be overridden by the default value of 3.
packages/connectors/src/webhook.ts-37-37 (1)

37-37: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use nullish coalescing to preserve explicit zero.

maxRetries || 3 treats 0 as falsy, so an explicit maxRetries: 0 (no retries) becomes 3. Use ?? to only default on null/undefined.

Proposed fix
-    this.endpoints.set(alias, { url, headers, hmacSecret, maxRetries: maxRetries || 3 });
+    this.endpoints.set(alias, { url, headers, hmacSecret, maxRetries: maxRetries ?? 3 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/connectors/src/webhook.ts` at line 37, The maxRetries parameter in
the endpoints.set call uses the logical OR operator which treats 0 as a falsy
value, causing an explicit maxRetries of 0 to default to 3 instead. Replace the
|| operator with the nullish coalescing operator ?? for the maxRetries
defaulting logic so that only null or undefined values default to 3, while
preserving an explicit 0 value for no retries.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7211fd8b-e70f-45c7-9dcf-95cbeddc11b4

📥 Commits

Reviewing files that changed from the base of the PR and between 66891b5 and 2edeef6.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (110)
  • .env.example
  • .gitignore
  • bin/alpclaw.mjs
  • bin/splash.mjs
  • bots/discord.ts
  • bots/lib/chat-agent.ts
  • bots/messenger.ts
  • bots/slack.ts
  • bots/telegram.ts
  • bots/whatsapp.ts
  • examples/cli.ts
  • examples/demo-standalone.ts
  • examples/package.json
  • package.json
  • packages/config/package.json
  • packages/config/src/global-store.ts
  • packages/config/src/index.ts
  • packages/config/src/loader.ts
  • packages/config/src/schema.ts
  • packages/connectors/package.json
  • packages/connectors/src/browser.ts
  • packages/connectors/src/connector.ts
  • packages/connectors/src/database.ts
  • packages/connectors/src/filesystem.test.ts
  • packages/connectors/src/filesystem.ts
  • packages/connectors/src/git.ts
  • packages/connectors/src/github.ts
  • packages/connectors/src/http.ts
  • packages/connectors/src/messaging.ts
  • packages/connectors/src/registry.ts
  • packages/connectors/src/terminal.ts
  • packages/connectors/src/webhook.ts
  • packages/core/package.json
  • packages/core/src/agent-loop.ts
  • packages/core/src/alpclaw.ts
  • packages/core/src/index.ts
  • packages/core/src/planner.ts
  • packages/core/src/runs/index.ts
  • packages/core/src/runs/run-manager.ts
  • packages/core/src/runs/run-store.ts
  • packages/core/src/self-corrector.ts
  • packages/core/src/task-manager.ts
  • packages/core/src/tui/app.tsx
  • packages/core/src/verifier.test.ts
  • packages/core/src/verifier.ts
  • packages/memory/package.json
  • packages/memory/src/episodic.ts
  • packages/memory/src/expanded-memory.test.ts
  • packages/memory/src/file-store.ts
  • packages/memory/src/manager.ts
  • packages/memory/src/memory.test.ts
  • packages/memory/src/profile.ts
  • packages/memory/src/skill-memory.ts
  • packages/memory/src/store.ts
  • packages/providers/package.json
  • packages/providers/src/cerebras.ts
  • packages/providers/src/claude.ts
  • packages/providers/src/cohere.ts
  • packages/providers/src/gemini.ts
  • packages/providers/src/groq.ts
  • packages/providers/src/index.ts
  • packages/providers/src/mistral.ts
  • packages/providers/src/nous.ts
  • packages/providers/src/nvidia.ts
  • packages/providers/src/ollama.ts
  • packages/providers/src/openai.ts
  • packages/providers/src/provider.ts
  • packages/providers/src/router.test.ts
  • packages/providers/src/router.ts
  • packages/safety/package.json
  • packages/safety/src/engine.test.ts
  • packages/safety/src/engine.ts
  • packages/safety/src/policies.ts
  • packages/safety/src/validator.ts
  • packages/skills/package.json
  • packages/skills/src/built-in/api-integrator.ts
  • packages/skills/src/built-in/code-edit.ts
  • packages/skills/src/built-in/code-reviewer.ts
  • packages/skills/src/built-in/config-editor.ts
  • packages/skills/src/built-in/data-analyst.ts
  • packages/skills/src/built-in/database-admin.ts
  • packages/skills/src/built-in/debugger.ts
  • packages/skills/src/built-in/deployer.ts
  • packages/skills/src/built-in/docs-generator.ts
  • packages/skills/src/built-in/git-helper.ts
  • packages/skills/src/built-in/linear-triage.ts
  • packages/skills/src/built-in/message-drafter.ts
  • packages/skills/src/built-in/notion-sync.ts
  • packages/skills/src/built-in/pr-creator.ts
  • packages/skills/src/built-in/python-runner.ts
  • packages/skills/src/built-in/repo-analysis.ts
  • packages/skills/src/built-in/shell-runner.ts
  • packages/skills/src/built-in/sql-builder.ts
  • packages/skills/src/built-in/subagent-runner.ts
  • packages/skills/src/built-in/task-queue.ts
  • packages/skills/src/built-in/task-summarizer.ts
  • packages/skills/src/built-in/test-runner.ts
  • packages/skills/src/built-in/web-scraper.ts
  • packages/skills/src/built-in/web-search.ts
  • packages/skills/src/index.ts
  • packages/skills/src/registry.ts
  • packages/skills/src/skill.ts
  • packages/utils/package.json
  • packages/utils/src/logger.ts
  • packages/utils/src/theme.ts
  • packages/utils/src/types.ts
  • tests/integration.test.ts
  • tsconfig.json
  • tsup.config.ts
  • vitest.config.ts
💤 Files with no reviewable changes (3)
  • bin/alpclaw.mjs
  • bin/splash.mjs
  • packages/core/src/alpclaw.ts

Comment thread examples/cli.ts
Comment on lines +1432 to +1435
console.log(pc.dim(`Installing ${pkg} into ${skillsDir}...`));
const child_process = await import("node:child_process");
child_process.execSync(`npm install ${pkg}`, { stdio: "inherit", cwd: skillsDir });
console.log(pc.green(`\n[OK] Installed ${pkg}. It will be loaded automatically.`));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Command injection risk: user-supplied package name passed to shell.

pkg comes directly from CLI args and is interpolated into a shell command. A malicious input like foo; rm -rf ~ would execute arbitrary commands. Use execFileSync with an argument array instead.

🔐 Secure alternative
-    child_process.execSync(`npm install ${pkg}`, { stdio: "inherit", cwd: skillsDir });
+    child_process.execFileSync("npm", ["install", pkg], { stdio: "inherit", cwd: skillsDir });
📝 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.

Suggested change
console.log(pc.dim(`Installing ${pkg} into ${skillsDir}...`));
const child_process = await import("node:child_process");
child_process.execSync(`npm install ${pkg}`, { stdio: "inherit", cwd: skillsDir });
console.log(pc.green(`\n[OK] Installed ${pkg}. It will be loaded automatically.`));
console.log(pc.dim(`Installing ${pkg} into ${skillsDir}...`));
const child_process = await import("node:child_process");
child_process.execFileSync("npm", ["install", pkg], { stdio: "inherit", cwd: skillsDir });
console.log(pc.green(`\n[OK] Installed ${pkg}. It will be loaded automatically.`));
🧰 Tools
🪛 OpenGrep (1.22.0)

[ERROR] 1434-1434: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/cli.ts` around lines 1432 - 1435, The `execSync` call in the code is
vulnerable to command injection because the user-supplied `pkg` variable is
interpolated directly into the shell command string. Replace the `execSync` call
with `execFileSync` instead, and pass the npm command and package as separate
array elements in the arguments array format rather than as a single
interpolated string. This ensures the package name is treated as a literal
argument and cannot be interpreted as additional shell commands. Maintain the
same options object with stdio and cwd properties.

Source: Linters/SAST tools

Comment on lines +88 to +91
if (issues.length === 0 && strategy) {
if (strategy === "exact" && output !== strategyOptions?.expected) {
issues.push(`Exact match failed. Expected: ${strategyOptions.expected}, Got: ${output}`);
suggestions.push("Check formatting or exact character sequence");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Prevent runtime crash when exact strategy is used without options.

This path dereferences strategyOptions.expected without a guard, so strategy: "exact" with missing options throws and aborts verification.

Suggested fix
-      if (strategy === "exact" && output !== strategyOptions?.expected) {
-        issues.push(`Exact match failed. Expected: ${strategyOptions.expected}, Got: ${output}`);
-        suggestions.push("Check formatting or exact character sequence");
-      } else if (strategy === "schema") {
+      if (strategy === "exact") {
+        const expected = strategyOptions?.expected;
+        if (typeof expected !== "string") {
+          issues.push("Exact verification requires strategyOptions.expected (string)");
+        } else if (String(output) !== expected) {
+          issues.push(`Exact match failed. Expected: ${expected}, Got: ${String(output)}`);
+          suggestions.push("Check formatting or exact character sequence");
+        }
+      } else if (strategy === "schema") {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/verifier.ts` around lines 88 - 91, The code in the exact
strategy validation block references strategyOptions.expected in the error
message without guarding against strategyOptions being undefined or null. The
comparison uses optional chaining but the string template does not, causing a
runtime crash when the exact strategy is used without options. Add a
null/undefined check for strategyOptions to ensure it exists before entering the
exact strategy block, either by adding strategyOptions to the condition that
checks strategy === "exact" or by wrapping the error message construction with a
proper guard.

Comment on lines +219 to +223
// Check if resolved path is within any of the allowed directories
const isAllowed = allowedDirs.some((dir) => {
const allowedResolved = path.resolve(dir);
return resolved.startsWith(allowedResolved);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix sandbox boundary validation to prevent path traversal bypass.

The current prefix check can incorrectly allow sibling paths (for example, an allowedDir of /sandbox also allows /sandbox-escape/...). This breaks the sandbox boundary.

🔒 Proposed fix
   const isAllowed = allowedDirs.some((dir) => {
     const allowedResolved = path.resolve(dir);
-    return resolved.startsWith(allowedResolved);
+    const rel = path.relative(allowedResolved, resolved);
+    return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
   });

Also applies to: 225-227

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/safety/src/engine.ts` around lines 219 - 223, The current path
traversal check using startsWith() in the allowedDirs.some() block is vulnerable
because it allows sibling paths (for example, /sandbox allows /sandbox-escape).
Fix this by ensuring the resolved path either equals the allowedResolved
directory exactly or is a child of it by checking that the next character after
the allowedResolved prefix is a path separator. Add proper boundary validation
logic that checks both that the resolved path starts with allowedResolved AND
that it's followed by a path separator character (or is an exact match). Apply
the same fix to the similar validation logic mentioned in lines 225-227.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant