Back to skills
SkillHub ClubShip Full StackFull Stack

pr-ship

Pre-ship risk report for OpenClaw PRs. Dynamically explores the codebase to assess module risk, blast radius, and version-specific gotchas. Scores each finding by severity (🟒/🟑/πŸ”΄). Updated frequently with the latest OpenClaw version context β€” run `clawhub update pr-ship` regularly to stay current.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
3,084
Hot score
99
Updated
March 20, 2026
Overall rating
C4.0
Composite score
4.0
Best-practice grade
C61.1

Install command

npx @skill-hub/cli install openclaw-skills-pr-ship

Repository

openclaw/skills

Skill path: skills/glucksberg/pr-ship

Pre-ship risk report for OpenClaw PRs. Dynamically explores the codebase to assess module risk, blast radius, and version-specific gotchas. Scores each finding by severity (🟒/🟑/πŸ”΄). Updated frequently with the latest OpenClaw version context β€” run `clawhub update pr-ship` regularly to stay current.

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: openclaw.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install pr-ship into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/openclaw/skills before adding pr-ship to shared team environments
  • Use pr-ship for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: pr-ship
description: Pre-ship risk report for OpenClaw PRs. Dynamically explores the codebase to assess module risk, blast radius, and version-specific gotchas. Scores each finding by severity (🟒/🟑/πŸ”΄). Updated frequently with the latest OpenClaw version context β€” run `clawhub update pr-ship` regularly to stay current.
---

# pr-ship

## Overview

Pre-ship risk report for **[OpenClaw](https://github.com/openclaw/openclaw)** pull requests.

This skill is **updated frequently** to track OpenClaw releases. The version-specific context (gotchas, behavioral changes, active risk areas) is refreshed with each upstream release. Run `clawhub update pr-ship` periodically to get the latest context.

What it does:
- Diffs your current branch against `main` in the OpenClaw repository.
- Dynamically investigates each changed module using grep/find/git on the codebase itself.
- Produces a structured risk report with evidence-backed findings scored by severity (🟒/🟑/πŸ”΄).
- No approve/reject gate -- just information for you to decide what to fix before publishing.

## Reference Layers

Load these files from the `references/` directory. Each serves a distinct purpose:

1. **`STABLE-PRINCIPLES.md`** -- Timeless OpenClaw coding standards: testing guide, file naming, safety invariants, common pitfalls, PR practices.

2. **`ARCHITECTURE-MAP.md`** -- OpenClaw structural context: module hierarchy, risk tier definitions with calibrated thresholds, critical path patterns, cross-module coupling, change impact matrix.

3. **`CURRENT-CONTEXT.md`** *(optional)* -- Version-specific gotchas, recent behavioral changes, and active risk areas. If this file exists, load it. It tracks the current OpenClaw release.

4. **`EXPLORATION-PLAYBOOK.md`** -- Dynamic investigation procedures. Read-only commands (grep, find, ls, git) that discover the current state of the OpenClaw codebase.

5. **`VISION-GUIDELINES.md`** -- Project vision, contribution policy, and merge guardrails derived from OpenClaw's VISION.md. Covers PR scope rules, security philosophy, plugin/core boundary, skills policy, MCP strategy, and the explicit "will not merge" list. Use this to catch policy and architectural misalignment.

STABLE-PRINCIPLES, ARCHITECTURE-MAP, EXPLORATION-PLAYBOOK, and VISION-GUIDELINES should always be present. CURRENT-CONTEXT is optional -- if missing, the skill still works but without version-specific gotcha awareness.

## Workflow

### 1. Load reference layers
- Read the four reference files listed above.

### 2. Collect diff against `main`
- Get current branch: `git branch --show-current`
- Gather file list: `git diff --name-only main...HEAD`
- Gather patch content: `git diff main...HEAD`

### 3. Classify changed modules
- For each changed file, identify its `src/<module>/` path.
- Look up the module's risk tier in ARCHITECTURE-MAP.md.
- If the module isn't listed or you want to verify, run the dynamic consumer count from EXPLORATION-PLAYBOOK.md "Dynamic Risk Classification" section.

### 4. Run dynamic exploration per changed module
- Follow EXPLORATION-PLAYBOOK.md "Blast Radius Discovery" for each changed file.
- Follow "Module-Specific Investigation Strategies" for each changed module type.
- Follow "Test Discovery" to identify relevant tests.
- Check "Red Flags Table" against the diff.

### 5. Evaluate findings
- Compare exploration evidence against:
  - Safety invariants and common pitfalls from STABLE-PRINCIPLES.md
  - Version-specific gotchas from CURRENT-CONTEXT.md (if loaded)
  - Architecture coupling patterns from ARCHITECTURE-MAP.md
  - Contribution policy, merge guardrails, and architectural direction from VISION-GUIDELINES.md
- Check PR scope against Vision contribution rules (one PR = one topic, size limits, bundling policy).
- Check for "will not merge" category matches from VISION-GUIDELINES.md section 7.
- Evaluate whether new capabilities respect the plugin/core boundary and security philosophy.
- Every finding must include:
  - **Evidence** from the diff (file + snippet)
  - **Exploration evidence** (command output showing blast radius, consumers, or pattern match)
  - **Reference** to the specific principle, gotcha, or coupling pattern it relates to

### 6. Produce report
- Use the report format below.
- Do not output "approved/rejected".

## Severity and Alert Scoring

- 🟒 **Low Risk** (score 1-2)
  Minor observation, style preference, or informational note. Safe to ship as-is.

- 🟑 **Attention Needed** (score 3-6)
  Partial mismatch, ambiguity, missing hardening, or non-blocking inconsistency. Worth reviewing but unlikely to cause breakage.

- πŸ”΄ **High Risk** (score 7-10)
  Clear conflict with OpenClaw coding standards, architecture patterns, or version-specific constraints. Likely to cause bugs, regressions, or policy violations.

Scoring:
- Score each finding individually (1-10).
- `final_alert_score = max(per_finding_scores)`. If no findings, `final_alert_score = 0`.

## Report Format

```markdown
## pr-ship report

- Branch: `<current-branch>`
- Base: `main`
- Files changed: `<N>`
- Modules touched: `<list with risk tiers>`
- Findings: `<N>`
- Final alert score: `<0-10>`

### Module Risk Summary

| Module | Risk Tier | Consumers | Files Changed |
| --- | --- | --- | --- |
| <module> | CRITICAL/HIGH/MEDIUM/LOW | <N> | <N> |

### Findings

1. 🟒/🟑/πŸ”΄ Title
   - Alert: `<1-10>`
   - Reference: `<principle, gotcha, or pattern from reference docs>`
   - Evidence in diff: `<file + short snippet/description>`
   - Exploration evidence: `<what dynamic investigation revealed>`
   - Why this matters: `<1-2 lines>`
   - Suggested fix: `<1-2 concrete actions>`

(repeat)

### Executive summary
- `<short practical summary for decision>`
- `<top 1-3 actions before publishing PR>`
```

## Constraints

- This skill is for the **OpenClaw repository only**. Do not use it on other projects.
- Review only the current branch diff against `main`.
- Do not review unrelated repository history.
- Do not auto-edit code unless explicitly asked.
- Do not convert the report into an approve/reject decision unless explicitly requested.
- Exploration commands are **read-only** (grep, find, ls, git diff). Never execute build, test, or code generation commands -- recommend them to the user in findings instead.

## Provenance

- **Source:** [github.com/Glucksberg/pr-ship](https://github.com/Glucksberg/pr-ship)
- **Maintainer:** Markus Glucksberg ([@Glucksberg](https://github.com/Glucksberg))
- **Update mechanism:** `CURRENT-CONTEXT.md` metadata is refreshed daily via cron when OpenClaw upstream `CHANGELOG.md` changes. GitHub repo is updated separately by the maintainer.
- **Verification:** The GitHub repo is the canonical source of truth. To verify your installed copy matches:

```bash
# Quick: compare file list + versions
diff <(clawhub list | grep pr-ship) <(curl -s https://api.github.com/repos/Glucksberg/pr-ship/contents/package.json | jq -r '.content' | base64 -d | jq -r .version)

# Full: diff your local install against GitHub
SKILL_DIR="$(find ~/.openclaw/skills -maxdepth 1 -name pr-ship -type d 2>/dev/null || echo skills/pr-ship)"
for f in SKILL.md package.json references/CURRENT-CONTEXT.md; do
  diff <(cat "$SKILL_DIR/$f") <(curl -s "https://raw.githubusercontent.com/Glucksberg/pr-ship/main/$f") && echo "$f: βœ” match" || echo "$f: ✘ differs"
done
```

## Security Notice

Reports generated by this skill may include diffs and grep output from your local repository. If your config files, environment, or code contain secrets (API keys, tokens, credentials), those values may appear in the report. **Do not publish or share generated reports without reviewing them for sensitive data first.**

## Credits

Original DEVELOPER-REFERENCE.md format and approach adapted from [mudrii](https://github.com/mudrii)'s developer reference methodology. The dynamic exploration approach was designed based on feedback from the OpenClaw maintainer community.


---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### README.md

```markdown
# pr-ship

Pre-ship risk report skill for [OpenClaw](https://github.com/openclaw/openclaw) pull requests. Dynamically explores the codebase to assess module risk, blast radius, and version-specific gotchas. Scores each finding by severity.

## Install

```bash
clawhub install pr-ship
```

## Update

```bash
clawhub update pr-ship
```

## How it works

1. Diffs your current branch against `main`
2. Classifies changed modules by risk tier
3. Runs dynamic exploration (grep, find, git) per module
4. Produces a structured risk report with evidence-backed findings

See [SKILL.md](SKILL.md) for the full workflow and report format.

## Provenance

- **Maintainer:** Markus Glucksberg ([@Glucksberg](https://github.com/Glucksberg))
- **Published on:** [ClawHub](https://clawhub.com/skills/pr-ship)
- **Update mechanism:** `CURRENT-CONTEXT.md` metadata is refreshed daily when OpenClaw upstream changes. This repo is updated separately by the maintainer.
- **Verification:** This GitHub repo is canonical. See [SKILL.md](SKILL.md#provenance) for commands to diff your local install against this repo.

## License

MIT

```

### _meta.json

```json
{
  "owner": "glucksberg",
  "slug": "pr-ship",
  "displayName": "Pr Ship",
  "latest": {
    "version": "1.0.13",
    "publishedAt": 1772581593699,
    "commit": "https://github.com/openclaw/skills/commit/4313b1b8c89f4fa7f01ea05067b81ffcf3491611"
  },
  "history": [
    {
      "version": "1.0.10",
      "publishedAt": 1772448891237,
      "commit": "https://github.com/openclaw/skills/commit/01bc6a62cb8dc7ae01347f042042513caa443178"
    },
    {
      "version": "1.0.7",
      "publishedAt": 1772240058174,
      "commit": "https://github.com/openclaw/skills/commit/e0016ac448010b6d757e2b4834ecbeaf4f2a85b4"
    },
    {
      "version": "1.0.4",
      "publishedAt": 1772189086835,
      "commit": "https://github.com/openclaw/skills/commit/5f0fef986e4016d8397197945a3591a41aa603b0"
    },
    {
      "version": "1.0.1",
      "publishedAt": 1772021393514,
      "commit": "https://github.com/openclaw/skills/commit/c40acad65a9a35796136da0fc82c182bfb3d5be0"
    },
    {
      "version": "1.0.9",
      "publishedAt": 1771756364947,
      "commit": "https://github.com/openclaw/skills/commit/0b6352b5654693af25d87b2d379dd20f8bfb2b75"
    },
    {
      "version": "2.0.3",
      "publishedAt": 1771653060962,
      "commit": "https://github.com/openclaw/skills/commit/8fe1388c7581248ed05ee6955c5f9700811368bb"
    },
    {
      "version": "2.0.1",
      "publishedAt": 1771651875661,
      "commit": "https://github.com/openclaw/skills/commit/598517874f9c4fdcf6b6845542290bed75d71999"
    }
  ]
}

```

### references/ARCHITECTURE-MAP.md

```markdown
# Architecture Map

> Structural context for understanding OpenClaw's module system, risk tiers, and critical paths.
> Describes concepts and patterns -- not specific file contents that drift with releases.

---

## 1. System Model: Hub-and-Spoke

OpenClaw follows a hub-and-spoke architecture:

- **Hub**: The gateway process orchestrates startup, configuration, routing, and agent execution
- **Spokes**: Channel implementations (Telegram, Discord, Slack, Signal, WhatsApp, iMessage, Line, Web) connect as leaf modules
- **Core**: Shared infrastructure (`config/`, `infra/`, `routing/`, `agents/`) is consumed by everything
- **Extensions**: Plugins, hooks, and skills extend behavior without modifying core

Messages flow inward from channel spokes through the routing hub to the agent runtime, then responses flow outward through the reply pipeline back to the originating channel.

---

## 2. Module Hierarchy

Modules are organized by dependency depth. Lower-level modules are more foundational and higher-risk.

| Level | Role | Modules |
| --- | --- | --- |
| 0 | **Foundation** -- pure types, config, utilities | `config/`, `shared/`, `utils/` |
| 1 | **Infrastructure** -- logging, process, errors | `infra/`, `logging/`, `process/` |
| 2 | **Routing layer** -- message routing, sessions, channels | `channels/`, `routing/`, `sessions/` |
| 3 | **Agent runtime** -- tool execution, model selection, streaming | `agents/` |
| 4 | **Reply pipeline** -- message orchestration, templating, thinking | `auto-reply/` |
| 5 | **Feature modules** -- memory, cron, hooks, plugins, security, browser, media, TTS | `memory/`, `cron/`, `hooks/`, `plugins/`, `security/`, `browser/`, `media/`, `tts/` |
| 6 | **Integration layer** -- gateway startup, protocol, server methods | `gateway/` |
| 7 | **Entry points** -- CLI, commands, TUI, daemon | `cli/`, `commands/`, `tui/`, `daemon/` |
| leaf | **Channel implementations** -- platform-specific SDK wrappers | `telegram/`, `discord/`, `slack/`, `signal/`, `whatsapp/`, `imessage/`, `line/`, `web/` |

---

## 3. Risk Tier Definitions

Risk tiers are determined by **production consumer count** -- how many files outside the module import from it.

| Tier | Threshold | Meaning | Required Validation |
| --- | --- | --- | --- |
| **CRITICAL** | 120+ prod consumers | Foundation modules. Breaking changes cascade to hundreds of files | Full test suite + all impacted subsystem tests |
| **HIGH** | 40-119 prod consumers | Heavily used infrastructure. Breaking changes affect dozens of files | Targeted subsystem tests + pre-PR checklist |
| **MEDIUM** | 10-39 prod consumers | Moderate reach. Changes affect specific subsystems | Module tests + caller verification |
| **LOW** | <10 prod consumers | Leaf/isolated modules. Changes are self-contained | Local tests + smoke checks |

### Dynamic Tier Discovery

To verify a module's current risk tier, count its external consumers:

```bash
# Count production files outside <module> that import from it
grep -r "from ['\"].*/<module>/" src/ --include="*.ts" -l | grep -v "/<module>/" | grep -v "\.test\." | wc -l
```

### Current Tier Assignments (calibrated Feb 2026)

| Tier | Modules |
| --- | --- |
| CRITICAL | `config` (666), `infra` (321), `agents` (247), `channels` (193), `routing` (149), `auto-reply` (132), top-level `runtime.ts` (309), `utils.ts` (205), `globals.ts` (160) |
| HIGH | `cli` (110), `logging` (93), `gateway` (79), `plugins` (59), `process` (55), `media` (48), `commands` (43), `shared` (40), `wizard` (40) |
| MEDIUM | `sessions` (30), `daemon` (29), `web` (25), `security` (24), `pairing` (23), `telegram` (23), `discord` (21), `markdown` (20), `slack` (18), `browser` (17), `hooks` (15), `signal` (12), `tts` (11), `imessage` (10) |
| LOW | `cron` (8), `plugin-sdk` (8), `compat` (7), `media-understanding` (7), `whatsapp` (7), `memory` (6), `canvas-host` (4), `line` (4), `providers` (4), all other leaf modules |

### High-Blast-Radius Files

These individual files have outsized impact. Changes to them affect many consumers.

| File | What It Exports |
| --- | --- |
| `config/config.ts` | `loadConfig()`, `OpenClawConfig`, `clearConfigCache()` |
| `config/types.ts` | All config type re-exports |
| `config/paths.ts` | `resolveStateDir()`, `resolveConfigPath()`, `resolveGatewayPort()` |
| `config/sessions.ts` | Session store CRUD |
| `infra/errors.ts` | Error utilities |
| `infra/json-file.ts` | `loadJsonFile()`, `saveJsonFile()` |
| `agents/agent-scope.ts` | `resolveDefaultAgentId()`, `resolveAgentWorkspaceDir()` |
| `channels/registry.ts` | `CHAT_CHANNEL_ORDER`, `normalizeChatChannelId()` |
| `routing/session-key.ts` | `buildAgentPeerSessionKey()`, `normalizeAgentId()` |
| `auto-reply/templating.ts` | `MsgContext`, `TemplateContext` types |
| `auto-reply/thinking.ts` | `ThinkLevel`, `VerboseLevel`, `normalizeVerboseLevel()` |
| `logging/subsystem.ts` | `createSubsystemLogger()` |

---

## 4. Critical Path Patterns

These are the core execution flows. Changes to files in these paths have amplified impact.

### Message Lifecycle: Inbound -> Response

```
Channel SDK event
-> channel bot-handlers / monitor
-> channel bot-message-context (normalize to MsgContext)
-> auto-reply/dispatch (dispatchInboundMessage)
-> routing/resolve-route (resolveAgentRoute)
-> auto-reply/reply pipeline (get-reply orchestrator)
   |- media understanding
   |- command auth
   |- session init
   |- directive parsing
   |- inline actions
   `- agent runner execution
-> agent runtime (system prompt, model selection, tools, streaming)
-> reply block pipeline (coalesce)
-> reply dispatcher (buffer + human delay)
-> channel outbound plugin (format + chunk)
-> channel send (API call)
```

### Tool Execution: Tool Call -> Result

```
LLM stream -> tool call extraction
-> tool registry lookup
-> pre-call hooks
-> tool policy pipeline (allow/deny/ask)
-> tool implementation
-> result truncation
-> result returned to LLM stream
```

### Config Loading: JSON -> Validated Object

```
Path resolution -> file read (JSON5, sync)
-> $include resolution -> env substitution
-> Zod validation -> legacy migration
-> defaults application -> runtime overrides
-> path normalization -> memory cache
```

---

## 5. Cross-Module Coupling Patterns

These coupling patterns are architectural -- they're not bugs but require awareness.

### Bidirectional Dependencies

- **`agents/` <-> `auto-reply/`**: By design. `agents/` provides runtime, `auto-reply/` orchestrates it. Changes to agent run result types affect reply delivery.
- **`config/` -> everything**: Config is imported by virtually every module. Config type changes have maximal blast radius.

### Hidden Coupling

- **`auto-reply/thinking.ts` -> `sessions/`**: `VerboseLevel` enum values are persisted in sessions. Changing enum values breaks session persistence.
- **`channels/dock.ts`**: Returns lightweight channel metadata without importing heavy channel code. Must stay in sync with channel capabilities.
- **`infra/outbound/deliver.ts`**: Used by both cron delivery AND message tool sends. Test both paths.
- **`routing/session-key.ts`**: Session key format is consumed by cron sessions, subagent sessions, and all route resolution. Format changes ripple everywhere.

### Barrel Export Amplification

Changes to `index.ts` barrel files amplify blast radius. A renamed or removed export from a barrel affects every consumer of that module, even if only one internal file changed.

### Change Impact Quick Reference

The highest-impact "if you change X, you MUST also check Y" mappings:

| If You Change... | You MUST Also Check... |
| --- | --- |
| `config/zod-schema*.ts` | ALL config validation tests, `config/defaults.ts`, JSON schema generation, every module reading the changed key |
| `config/types*.ts` | Every file importing the changed type (grep!), Zod schema must match |
| `routing/session-key.ts` | Session key parsing everywhere, cron sessions, subagent sessions |
| `agents/pi-tools.ts` | ALL tool tests, tool policy, tool display, sandbox tool policy |
| `agents/pi-embedded-runner/run/` | Entire agent execution path, fallback, compaction, streaming |
| `auto-reply/dispatch.ts` | All channel inbound paths |
| `auto-reply/reply/get-reply.ts` | The entire reply pipeline -- most impactful single file |
| `auto-reply/templating.ts` | `MsgContext` type used by 15+ files |
| `channels/registry.ts` | Channel normalization, routing, dock, all channel references |
| `gateway/protocol/schema/*.ts` | WS protocol compat, CLI client, TUI. User should run `pnpm protocol:gen:swift` before PR |
| Any `index.ts` barrel | All consumers of that module's exports |

For the full list, use EXPLORATION-PLAYBOOK.md's dynamic investigation commands.

---

## 6. Key Design Patterns

| Pattern | Where Used | Implication |
| --- | --- | --- |
| Zod schema + type file pairs | `config/` | Schema and types must stay in sync |
| Channel plugin contract | `channels/plugins/` | All channels implement `ChannelPlugin` interface |
| Hook frontmatter metadata | `hooks/bundled/` | `HOOK.md` frontmatter controls hook loading |
| Plugin discovery + manifest | `plugins/` | `openclaw.plugin.json` manifest drives discovery |
| Session key hierarchy | `routing/` | Hierarchical key format encodes agent/channel/peer |
| Tool policy pipeline | `agents/` | allow/deny/ask flow before tool execution |
| Config caching with `WeakMap` | `routing/` | Cache keyed on config object identity |
| Subsystem logger factory | `logging/` | All runtime logging through `createSubsystemLogger()` |
| File locking for stores | `sessions/`, `cron/` | Concurrent access serialized via lock wrappers |
| `locked()` serialization | `cron/service/` | Cron operations serialized to prevent races |
| Streaming state machine | `agents/pi-embedded-subscribe` | SSE chunk processing with stateful parsing |
| Extension factory pattern | `agents/pi-embedded-runner/` | Extensions loaded as factory functions, not file paths |
| Reply block coalescing | `auto-reply/reply/` | Multiple LLM outputs merged into delivery blocks |
| Outbound adapter pattern | `channels/plugins/outbound/` | Per-channel formatting + chunking |
| Gateway startup sequence | `gateway/` | Ordered subsystem initialization |

---

## 7. Config Architecture

Configuration uses a layered system:

1. **File**: `openclaw.json` (JSON5 format, supports comments and trailing commas)
2. **Includes**: `$include` directives (confined to config directory)
3. **Environment**: `${ENV_VAR}` substitution
4. **Validation**: Zod schemas enforce types
5. **Migration**: Legacy format auto-migration
6. **Defaults**: Module-specific defaults applied
7. **Runtime overrides**: Environment variable overrides
8. **Cache**: In-memory with `clearConfigCache()` invalidation

### Adding a Config Key

1. Add type to `config/types.*.ts`
2. Add Zod schema to `config/zod-schema.*.ts` (must match type)
3. Add default in `config/defaults.ts` if applicable
4. Update `config/schema.hints.ts` for UI labels
5. Add test in `config/config.*.test.ts`
6. If migrating: add rule in `config/legacy.migrations.part-*.ts`

```

### references/CURRENT-CONTEXT.md

```markdown
# CURRENT-CONTEXT.md β€” OpenClaw Version-Specific Gotchas

_Auto-updated by fork-manager sync. Last updated: 2026-03-03T23:21 UTC (upstream sync)_

---

## Active Version: 2026.3.3 (Released)

### Behavioral Changes (Breaking / Semantics)

- **BREAKING: Onboarding defaults `tools.profile` to `messaging`** for new local installs. New setups no longer start with broad coding/system tools unless explicitly configured.
- **BREAKING: ACP dispatch enabled by default** unless `acp.dispatch.enabled=false`. Pause routing explicitly if needed.
- **BREAKING: Plugin SDK removed `api.registerHttpHandler()`** β€” must use `api.registerHttpRoute({ path, auth, match, handler })`.
- **BREAKING: Zalouser no longer depends on external zca CLI binaries.** Use `openclaw channels login --channel zalouser` after upgrade.
- **BREAKING: Node exec approval payloads require `systemRunPlan`.** Rejection on missing plan.
- **BREAKING: Node `system.run` canonical path pinning.** Path-token commands resolve to `realpath`.
- **Secrets/SecretRef overhaul:** 64-target coverage, runtime collectors, fail-fast on active surfaces, non-blocking diagnostics on inactive.
- **Tools/PDF analysis:** First-class `pdf` tool with native Anthropic/Google PDF provider support.
- **Memory/Ollama embeddings:** `memorySearch.provider = "ollama"` support.
- **Telegram/Streaming defaults:** `channels.telegram.streaming` now defaults to `partial` (from `off`).
- **Sessions/Attachments:** Inline file attachment support for `sessions_spawn` with base64/utf8 encoding.
- **Plugin SDK/channel extensibility:** `channelRuntime` exposed on `ChannelGatewayContext`.
- **Plugin hooks/session lifecycle:** `sessionKey` in `session_start`/`session_end` hook events.
- **CLI/Banner taglines:** `cli.banner.taglineMode` (`random`|`default`|`off`) config.
- **Media understanding/audio echo:** `tools.media.audio.echoTranscript` + `echoFormat` config.

### New Gotchas (Things That Now Behave Differently)

- **Diffs guidance loading:** Moved from unconditional prompt-hook injection to plugin companion skill path.
- **Gateway/Plugin HTTP route precedence:** Plugin routes run before Control UI SPA catch-all.
- **Security/auth labels:** `/status` and `/models` no longer expose credential fragments β€” model-auth-label.ts updated.
- **Discord typing indicators:** Stopped after NO_REPLY/silent runs β€” affects PRs that check typing state post-reply.
- **Discord slash commands:** Intercepted and registered as native commands β€” text-based slash PRs must align.
- **Exec heartbeat routing:** Scoped wakes β€” PRs adding exec triggers must specify agent session keys to avoid cross-agent wakeup.
- **Agents/Compaction:** Staged-summary merge now preserves in-flight task context β€” compaction PRs must not strip active state.
- **Session startup date grounding:** YYYY-MM-DD placeholders auto-substituted β€” no need to inject dates manually in AGENTS context.
- **Gateway/Channel health monitor:** Startup-connect grace window prevents restart thrash for channels that just (re)started.
- **Slack/Bolt 4.6+:** Removed invalid `message.channels`/`message.groups` event registrations.
- **Slack/socket auth:** Fail fast on non-recoverable auth errors instead of retry-looping.
- **Feishu/group broadcast dispatch:** Multi-agent group broadcast with observer-session isolation.
- **Feishu/inbound debounce:** Same-chat sender bursts debounced into single dispatch.
- **Voice-call/runtime lifecycle:** `EADDRINUSE` loop prevention, idempotent webhook `start()`.
- **Exec approvals/allowlist:** Regex metacharacters escaped in path-pattern literals.
- **Browser profile defaults:** `openclaw` profile preferred over `chrome` in headless/no-sandbox.
- **Browser act request compatibility:** Legacy flattened `action="act"` params accepted.
- **Docker sandbox bootstrap hardening:** `OPENCLAW_SANDBOX` opt-in parsing, rollback on partial failures.
- **Gateway/WS security:** `ws://` loopback-only by default.
- **Security/Prompt spoofing hardening:** Runtime events use system-prompt context, spoof markers neutralized.
- **Tools/Diffs:** PDF file output support and quality customization.
- **CLI/Config:** `openclaw config validate` with `--json`.

### High-Risk Modules (Frequently Changed in 2026.3.x)

| Module Prefix | Reason |
|---|---|
| src/telegram/ | Streaming defaults, DM topics, multi-account routing, implicit mention forum handling |
| extensions/feishu/ | 20+ fixes: broadcast dispatch, debounce, topic routing, file uploads, typing, probes |
| src/security/ | SSRF guards, ACP sandbox inheritance, prompt spoofing, webhook hardening, fs-safe writes |
| src/browser/ | Extension relay reconnect, profile defaults, CDP startup, act compat, managed tab cap |
| extensions/slack/ | Bolt 4.6 compat, socket auth fail-fast, thread context, session routing, debounce |
| src/gateway/ | Plugin HTTP routes, Control UI basePath, WS security, channel health monitor |
| extensions/line/ | Auth boundary hardening synthesis, media download, context/routing, status/config |
| src/agents/ | Compaction continuity, skills runtime loading, subagent sessions |
| src/sandbox/ | Bootstrap boundary hardening, workspace mount perms, Docker setup commands |
| src/cli/ | Config validate, banner taglines, browser timeout |

### Pre-PR Checklist Additions (Version-Specific)

- **Node exec PRs:** Must include `systemRunPlan` in approval payloads and accept canonical paths.
- **Browser PRs:** Check profile defaults (openclaw vs chrome), act request format compatibility, CDP startup diagnostics.
- **Feishu PRs:** Multi-account routing, rich-text post parsing, docx API changes, media type classification.
- **Telegram PRs:** DM topic routing, reply media context, outbound chunk splitting.
- **Docker PRs:** Sandbox opt-in parsing, socket paths, health check probes, systemd container detection.
- **Cron PRs:** Light bootstrap mode, announce delivery status, session routing for reminders.
- **Security PRs:** Prompt spoofing neutralization, WS loopback enforcement, webhook path validation.

---

## Recent Behavioral Changes (Rolling Window: Last 4 Versions)

### 2026.3.3

- **4 BREAKING changes:** Onboarding tools.profile=messaging default, ACP dispatch enabled by default, plugin SDK registerHttpHandler removed, Zalouser native zca-js.
- **Secrets/SecretRef overhaul** with 64-target coverage and fail-fast semantics.
- **LINE synthesis mega-fixes:** Auth boundary, media download, context/routing, status/config (9 merged synthesis PRs).
- **Feishu 20+ fixes:** Broadcast dispatch, debounce, topic routing, file uploads, typing, probes.
- **Security hardening wave:** ACP sandbox inheritance, SSRF guards, prompt spoofing, fs-safe writes, webhook req hardening, skills archive extraction.
- **Telegram streaming defaults** to `partial`, implicit mention forum handling.
- **Slack/Bolt 4.6+** message event compat, socket auth fail-fast, debounce routing.
- **Security/auth labels (#33262):** Token/API-key snippets removed from `/status` and `/models` β€” PRs touching model-auth-label.ts must not re-expose credential fragments.
- **Gateway/session agent discovery (#32831):** Disk-scanned agent IDs included in `listConfiguredAgentIds` even when `agents.list` is configured.
- **Discord mega-wave:** Presence defaults (online on ready), typing cleanup after NO_REPLY, mention formatting+rewrites, media SSRF CDN allowlist, chunk ordering/retry, voice messages JSON upload, Opus→opusscript fallback, slash command native registration, thread session lifecycle reset on archive, audit wildcard fix, inbound debouncer bot-own skip.
- **Telegram/DM draft finalization (#32118):** Requires verified final-text emission before marking delivered; falls back to normal send.
- **Telegram/draft preview boundary (#33169):** Answer-lane boundary stabilization; NO_REPLY lead-fragment suppression.
- **Telegram/device pairing notifications (#33299):** Auto-arm on `/pair qr`, auto-ping on new requests.
- **Exec heartbeat routing (#32724):** Heartbeat wakes scoped to agent session keys β€” unrelated agents no longer woken.
- **Agents/Compaction continuity (#8903):** Staged-summary merge preserves active task status, batch progress, latest user request.
- **Agents/Session startup date grounding (#32381):** `YYYY-MM-DD` placeholders in AGENTS context substituted at startup and post-compaction.
- **Discord/allowBots mention gating:** `allowBots: "mentions"` only accepts bot messages that mention the bot.
- **Tools/Diffs guidance loading (#32630):** Moved from unconditional prompt-hook to plugin companion skill path.

### 2026.3.2

- **OpenAI Responses WS-first default** with SSE fallback and warm-up option.
- **Telegram DM topics** with per-DM config and topic-aware routing.
- **Subagent typed completion events** (`task_completion`) replacing system-message handoff.
- **Node exec canonical path pinning** and `systemRunPlan` requirement.
- **Browser profile defaults** prefer `openclaw` in headless environments.
- **Docker/Container probes** (`/healthz`, `/readyz`) and sandbox bootstrap hardening.

### 2026.2.27

- **German locale** support in Web UI.
- **Discord thread lifecycle** inactivity-based controls.
- **Android nodes** camera, device, notifications actions.
- **Security** webhook rate-limit state bounding.

### 2026.2.26

- **Heartbeat `directPolicy` reverted** back to `allow` (was `block` in 2026.2.24-25).
- **Secrets management** workflow (`audit`, `configure`, `apply`, `reload`).
- **OpenAI Codex** WebSocket-first transport.
- **Agent binding CLI** (`openclaw agents bindings/bind/unbind`).

---

## Foundational Gotchas (Always Apply)

- **Package manager:** Always use `pnpm` β€” not `npm`, not `bun`. Build: `pnpm build`. Install: `pnpm install`.
- **Branch force push:** Use `--force-with-lease`, never `--force`.
- **PM2 for gateway:** `pm2 restart openclaw` β€” never `kill <pid>` or `systemctl restart`.
- **Test files:** Must be co-located with source as `<module>.test.ts` β€” not in a `__tests__/` dir.
- **No `any` types** in production code paths. TypeScript strictness applies.
- **Channel plugins must remain stateless across reconnects** β€” no persistent mutable module-level state.

```

### references/EXPLORATION-PLAYBOOK.md

```markdown
# Exploration Playbook

> Dynamic investigation procedures for PR review. These commands discover the current state
> of the codebase rather than relying on hardcoded file paths or static references.
> All discovery uses `grep`, `find`, `ls`, `wc`, and `git` -- never hardcoded file paths.

---

## 1. Module Identification

For each file in the diff, classify it by its `src/<module>/` path:

```bash
# List all changed files, grouped by module
git diff --name-only <base>...HEAD | grep '^src/' | sed 's|src/\([^/]*\)/.*|\1|' | sort -u
```

If a file is in a channel directory (e.g., `src/telegram/`, `src/discord/`), it's a **leaf module**.
If it's a top-level `src/*.ts` file (e.g., `runtime.ts`, `utils.ts`), treat it as **CRITICAL** -- these have 150-300+ consumers.

---

## 2. Dynamic Risk Classification

Don't rely on static risk tables. Count actual consumers to classify each changed module:

```bash
# Count production files outside <module> that import from it
grep -r "from ['\"].*/<module>/" src/ --include="*.ts" -l | grep -v "/<module>/" | grep -v "\.test\." | wc -l
```

### Risk Tier Thresholds

| Tier | Consumer Count | Action |
| --- | --- | --- |
| CRITICAL | 120+ | Full test suite required. Check all critical path flows. |
| HIGH | 40-119 | Targeted subsystem tests + pre-PR checklist. |
| MEDIUM | 10-39 | Module tests + verify direct callers. |
| LOW | <10 | Local tests + smoke check. |

### Quick Classification for Known Modules

Some modules are structurally stable in their tier. Use these as starting points, but verify with grep if the diff touches exports:

- **Always CRITICAL**: `config/`, `infra/`, `agents/`, `channels/`, `routing/`, `auto-reply/`
- **Always HIGH**: `cli/`, `logging/`, `gateway/`, `plugins/`, `process/`
- **Leaf/LOW to MEDIUM**: Individual channel implementations (`telegram/`, `discord/`, etc.). Most are MEDIUM by consumer count (10-39). Truly isolated ones (`whatsapp/`, `line/`) are LOW. Verify with grep if the diff touches shared types.

---

## 3. Blast Radius Discovery

For each changed file, determine what breaks if it changes:

### Step 1: Identify Exported Symbols

```bash
# Find all exports from the changed file
grep -n "^export " <changed-file>
```

### Step 2: Find Consumers

```bash
# Find all files that import from the changed file's module
grep -r "from ['\"].*/<module>/" src/ --include="*.ts" -l | grep -v "\.test\."
```

### Step 3: Check Barrel Exports

If the changed file's exports are re-exported through an `index.ts` barrel:

```bash
# Check if the module has a barrel
ls src/<module>/index.ts 2>/dev/null

# If yes, check what it re-exports from the changed file
grep "<changed-filename>" src/<module>/index.ts
```

**Barrel amplification**: If the changed export appears in the barrel, ALL consumers of that barrel are potentially affected, not just direct importers.

### Step 4: Cross-Module Dependencies

```bash
# Find which other modules import from this module
grep -r "from ['\"].*/<module>/" src/ --include="*.ts" -l | grep -v "/<module>/" | sed 's|src/\([^/]*\)/.*|\1|' | sort -u
```

---

## 4. Module-Specific Investigation Strategies

When a diff touches files in these modules, run the corresponding investigation:

### `config/`

```bash
# Find the Zod schema for a changed type
grep -r "Schema" src/config/zod-schema*.ts --include="*.ts" -l

# Check if a new config key has a matching Zod schema
# (type file and schema file must stay in sync)
grep -n "<key-name>" src/config/types.*.ts
grep -n "<key-name>" src/config/zod-schema.*.ts

# Check if defaults exist for the changed key
grep -n "<key-name>" src/config/defaults.ts

# Find all consumers of the changed config key
grep -r "<key-name>" src/ --include="*.ts" -l | grep -v "\.test\."
```

**Red flags**: Type without matching schema. Schema without matching type. Missing default. Changed key with no migration rule.

### `agents/`

```bash
# Check tool registration for changed tools
grep -n "registerTool\|defineTool\|openclaw-tools" src/agents/tools/*.ts

# Check tool policy for the changed tool
grep -rn "<tool-name>" src/agents/tool-policy*.ts

# Check system prompt impact
grep -rn "system-prompt\|systemPrompt" src/agents/ --include="*.ts" | head -20

# Check subagent depth/spawn constraints
grep -rn "maxSpawnDepth\|maxChildren\|subagent-depth" src/agents/ --include="*.ts"
```

**Red flags**: Tool registered but no policy. Changed tool not in policy pipeline. System prompt changes without LLM testing.

### `auto-reply/`

```bash
# Check bidirectional deps with agents/
grep -r "from ['\"].*agents/" src/auto-reply/ --include="*.ts" -l
grep -r "from ['\"].*auto-reply/" src/agents/ --include="*.ts" -l

# Check template context consumers
grep -r "MsgContext\|TemplateContext" src/ --include="*.ts" -l | grep -v "\.test\."

# Check thinking/verbose level consumers
grep -r "ThinkLevel\|VerboseLevel" src/ --include="*.ts" -l | grep -v "\.test\."
```

**Red flags**: Changed `MsgContext` type (15+ consumers). Changed `VerboseLevel` enum (breaks session persistence). Reply pipeline changes without fallback path testing.

### `channels/` (any channel module)

```bash
# Verify ChannelPlugin compliance
grep -n "ChannelPlugin\|implements.*Channel" src/<channel>/ --include="*.ts" -r

# Check outbound adapter
ls src/channels/plugins/outbound/<channel>*.ts 2>/dev/null

# Check channel registry
grep -n "<channel>" src/channels/registry.ts

# Check dock metadata
grep -n "<channel>" src/channels/dock.ts 2>/dev/null
```

**Red flags**: Channel capability change without dock update. Missing outbound adapter. Broken ChannelPlugin contract.

### `routing/`

```bash
# Check session key format consumers
grep -r "sessionKey\|session-key\|buildAgentPeerSessionKey\|normalizeAgentId" src/ --include="*.ts" -l | grep -v "\.test\."

# Check route resolution consumers
grep -r "resolveAgentRoute\|resolveRoute\|resolve-route" src/ --include="*.ts" -l | grep -v "\.test\."
```

**Red flags**: Session key format changes ripple to cron, subagents, and all route resolution.

### `gateway/`

```bash
# Check protocol schema for breaking changes
ls src/gateway/protocol/schema/*.ts

# Check startup sequence dependencies
grep -rn "server-startup\|serverStartup\|startGateway" src/gateway/ --include="*.ts" | head -10

# Check auth changes
grep -rn "gateway.auth\|auth.mode\|auth.token" src/ --include="*.ts" -l | head -20
```

**Red flags**: Protocol schema changes break CLI/TUI clients. Startup order changes can break subsystem initialization. Auth changes need `openclaw security audit`.

### `hooks/`

```bash
# Check hook loading pipeline
grep -rn "loadInternalHooks\|loadWorkspaceHookEntries\|registerInternalHook" src/ --include="*.ts" -l

# Check bundled hooks
ls src/hooks/bundled/*/handler.ts 2>/dev/null

# Check hook frontmatter parsing
grep -rn "frontmatter\|HOOK.md" src/hooks/ --include="*.ts"
```

**Red flags**: Hook loading order changes. Missing `HOOK.md` frontmatter. Changed hook registration interface.

### `plugins/`

```bash
# Check plugin discovery
grep -rn "discoverOpenClawPlugins\|openclaw.plugin.json" src/plugins/ --include="*.ts"

# Check plugin SDK contract
grep -rn "definePlugin\|PluginRegistry" src/plugins/ --include="*.ts"

# Check plugin loader
grep -rn "loadOpenClawPlugins\|jiti" src/plugins/ --include="*.ts"
```

**Red flags**: Changed plugin manifest format. Broken `definePlugin()` contract. Discovery path changes.

### `security/`

```bash
# Check SSRF guard patterns
grep -rn "ssrf\|SsrfGuard\|fetchWithSsrf" src/ --include="*.ts" -l

# Check audit patterns
grep -rn "security.*audit\|securityAudit" src/ --include="*.ts" -l

# Check sandbox patterns
grep -rn "sandbox\|safeBins\|trustedBin" src/ --include="*.ts" -l
```

**Red flags**: Any security change requires `openclaw security audit` before PR. SSRF guard bypass. Sandbox escape.

---

## 5. Test Discovery

Tests are co-located with source files. Use algorithmic discovery:

```bash
# Find unit tests for changed files
for f in $(git diff --name-only <base>...HEAD | grep '\.ts$' | grep -v '\.test\.'); do
  test_file="${f%.ts}.test.ts"
  [ -f "$test_file" ] && echo "$test_file"
done

# Find E2E tests for changed modules
for module in $(git diff --name-only <base>...HEAD | grep '^src/' | sed 's|src/\([^/]*\)/.*|\1|' | sort -u); do
  find "src/$module" -name "*.e2e.test.ts" 2>/dev/null
done

# Find test harnesses for the changed module
for module in $(git diff --name-only <base>...HEAD | grep '^src/' | sed 's|src/\([^/]*\)/.*|\1|' | sort -u); do
  find "src/$module" -name "*.test-harness.ts" 2>/dev/null
done

# Discover test helpers
ls src/test-helpers/ src/test-utils/ 2>/dev/null
```

### Test Execution Priority

1. Run co-located unit tests for changed files first
2. Run module-level tests for changed modules
3. For CRITICAL/HIGH modules: run full suite (`pnpm test`)
4. For config changes: run config-specific tests + read-back validation

---

## 6. Red Flags Table

Pattern-based signals that indicate elevated risk, regardless of module:

| Signal | Detection | Why It's Risky |
| --- | --- | --- |
| `async` keyword removed from exported function | `git diff` shows `-async` on export line | Changes return type from `Promise<T>` to `T`. All `await` callers break silently. |
| Barrel `index.ts` modified | `git diff --name-only` includes `index.ts` | All consumers of that module's exports are affected. |
| Cache/memoization pattern changed | Diff contains `WeakMap`, `cache`, `memoize`, `clearCache` | Cache invalidation bugs are subtle and hard to test. |
| Session key format touched | Diff contains `sessionKey`, `buildAgentPeer`, `normalizeAgentId` | Format changes ripple to cron, subagents, route resolution. |
| Enum value changed | Diff modifies `enum` or union literal type | Persisted enum values in sessions/config break on change. |
| `loadConfig()` added in hot path | Diff adds `loadConfig()` in `auto-reply/`, channel send, streaming | Sync disk I/O in hot path. Performance regression. |
| `console.log/warn/error` in runtime code | Diff adds `console.*` outside tests | Must use subsystem logger for runtime paths. |
| Try/catch on primary operation | Diff adds try/catch around core logic (not convenience wrapper) | Swallowing errors on critical paths hides bugs. |
| File locking removed | Diff removes `locked()`, `withLock`, lock wrappers | Race conditions in concurrent session/cron access. |
| `--force` in git operation | Diff or command uses `git push --force` | Data loss risk. Must use `--force-with-lease`. |
| New tool without policy entry | New tool file without matching policy configuration | Tool runs without access controls. |
| Protocol schema changed | Diff touches `gateway/protocol/schema/*.ts` | CLI/TUI client compatibility. Recommend running `pnpm protocol:gen:swift` + `pnpm protocol:check` before PR. |

---

## 7. Pre-PR Command Verification

**Never hardcode build/test commands.** Always source them from the project:

```bash
# Discover available commands from package.json
grep -A1 '"scripts"' package.json | head -30
# Or more specifically:
node -e "const p=require('./package.json'); Object.keys(p.scripts).forEach(k=>console.log(k+': '+p.scripts[k]))"

# Discover CI commands from workflow
grep -n "run:" .github/workflows/ci.yml | head -30
```

### Standard Commands (recommend to the user, do NOT execute)

These are commands the **user** should run before submitting a PR. List them in findings as suggested actions, but do not execute them -- they modify files or run long processes.

- `pnpm build` -- TypeScript compilation
- `pnpm check` -- Format + type check + lint
- `pnpm test` -- Full test suite
- `pnpm check:docs` -- Documentation validation
- `pnpm protocol:check` -- Protocol compatibility
- `pnpm protocol:gen:swift` -- Regenerate Swift protocol (after schema changes, generates files)

### Conditional Recommendations

Recommend these in findings when the corresponding files are in the diff:

- Protocol schema changes: recommend `pnpm protocol:gen:swift && pnpm protocol:check`
- Docs changes: recommend `pnpm check:docs && pnpm format:check -- <changed-doc>`
- Config changes: recommend `pnpm vitest run src/config/`

---

## 8. Cross-Reference Workflow

When investigation reveals a concerning pattern, cross-reference with:

1. **CURRENT-CONTEXT.md** -- Check if a recent release introduced new constraints or gotchas relevant to the changed module
2. **STABLE-PRINCIPLES.md** -- Verify the change doesn't violate safety invariants or common pitfall patterns
3. **ARCHITECTURE-MAP.md** -- Understand the module's position in the hierarchy and its coupling patterns

The exploration playbook produces **evidence**. The reference documents provide **judgment criteria**. The report combines both.

```

### references/STABLE-PRINCIPLES.md

```markdown
# Stable Principles

> Timeless coding standards for OpenClaw contributions. These principles rarely change.
> Extracted from project conventions and contributor experience.

---

## 1. Testing Guide

### Running Tests

```bash
# Full suite (parallel runner, matches CI)
pnpm test

# Single module (direct vitest for targeted runs)
pnpm vitest run src/config/

# Single file
pnpm vitest run src/config/io.test.ts

# Watch mode
pnpm vitest src/config/io.test.ts

# With coverage
pnpm vitest run --coverage
```

### Test Framework: Vitest

- Config: `vitest.config.ts` at project root
- Mocking: `vi.mock()`, `vi.fn()`, `vi.spyOn()`
- Assertions: `expect()` with Vitest matchers

### Test Patterns

| Pattern      | Example                                |
| ------------ | -------------------------------------- |
| Unit test    | `src/config/io.test.ts`                |
| E2E test     | `src/cli/program.smoke.e2e.test.ts`    |
| Test harness | `src/cron/service.test-harness.ts`     |
| Test helpers | `src/test-helpers/`, `src/test-utils/` |
| Mock file    | `src/cron/isolated-agent.mocks.ts`     |

### CI Pipeline

- **Fail-fast scoping gates run first**: `docs-scope` and `changed-scope` determine whether heavy Node/macOS/Android jobs run.
- **Docs-only PRs skip heavy lanes** and run `check-docs` instead.
- **Node quality gate**: `pnpm check` remains the required type+lint+format gate.
- **Build artifact reuse**: `build-artifacts` builds `dist/` once and downstream jobs consume it.

---

## 2. File Naming Conventions

### Within Modules

```
src/<module>/
β”œβ”€β”€ index.ts                    # Barrel re-exports (public API)
β”œβ”€β”€ types.ts                    # Type definitions
β”œβ”€β”€ *.ts                        # Implementation files
β”œβ”€β”€ *.test.ts                   # Co-located unit tests
β”œβ”€β”€ *.e2e.test.ts               # End-to-end tests
β”œβ”€β”€ *.test-harness.ts           # Reusable test fixtures
β”œβ”€β”€ *.mocks.ts                  # Test mocks
```

### Naming Patterns

| Pattern             | Meaning                             | Example                     |
| ------------------- | ----------------------------------- | --------------------------- |
| `*.test.ts`         | Unit test                           | `io.test.ts`                |
| `*.e2e.test.ts`     | End-to-end test                     | `program.smoke.e2e.test.ts` |
| `*.test-harness.ts` | Reusable test fixture               | `service.test-harness.ts`   |
| `*.mocks.ts`        | Test mock definitions               | `isolated-agent.mocks.ts`   |
| `*.impl.ts`         | Implementation (when barrel exists) | `auto-reply.impl.ts`        |
| `zod-schema.*.ts`   | Zod validation schema               | `zod-schema.agents.ts`      |
| `types.*.ts`        | Domain-specific types               | `types.telegram.ts`         |

### Where to Put New Files

| Adding a...            | Put it in...                                                           |
| ---------------------- | ---------------------------------------------------------------------- |
| New tool               | `src/agents/tools/<tool-name>.ts` + register in `openclaw-tools.ts`    |
| New channel            | `src/<channel>/` + `extensions/<channel>/` for plugin                  |
| New CLI command        | `src/cli/<command>-cli.ts` + `src/commands/<command>.ts`               |
| New config type        | `src/config/types.<section>.ts` + `src/config/zod-schema.<section>.ts` |
| New hook               | `src/hooks/bundled/<hook-name>/handler.ts` + `HOOK.md`                 |
| New gateway RPC method | `src/gateway/server-methods/<method>.ts`                               |
| New test               | Co-locate with source: `src/<module>/<file>.test.ts`                   |

---

## 3. Safety Invariants (Never Violate)

1. Never call `loadConfig()` in hot paths (`auto-reply/*`, channel send paths, streaming handlers).
2. Use subsystem logger instead of `console.*` in runtime paths.
3. Do exact identity checks (`a === b`), not truthy co-existence checks.
4. Treat primary operations as fail-fast (throw); only convenience wrappers should catch.
5. Keep file locking for sessions/cron stores; do not remove lock wrappers.
6. For security-sensitive changes (auth, SSRF, exec), run `openclaw security audit` before PR.
7. For config mutations, patch full nested path and verify by immediate read-back.

---

## 4. Common Pitfalls

1. **Never call `loadConfig()` in render/hot paths** - it does sync `fs.readFileSync`. Thread config through params.
2. **Verify function is actually `async` before adding `await`** - causes `await-thenable` lint errors.
3. **Removing `async` from exported functions is BREAKING** - changes return type from `Promise<T>` to `T`. All `await` callers break.
4. **Primary operations must throw; only convenience ops get try/catch** - don't swallow errors on critical paths.
5. **Guard numeric comparisons against NaN** - use `Number.isFinite()` before `>` / `<`.
6. **Normalize paths before string comparison** - `path.resolve()` before `===`.
7. **Derive context from parameters, not global state** - use explicit paths, not env var fallbacks.
8. **Run FULL `pnpm lint` before every push** - not just changed files. Type-aware linting catches cross-file issues.

---

## 5. Commit Message Conventions

- `feat:` - New feature
- `fix:` - Bug fix
- `perf:` - Performance improvement
- `refactor:` - Code restructuring
- `test:` - Test additions/changes
- `docs:` - Documentation

---

## 6. PR & Bug Filing Best Practices

### Multi-Model Review

- Use multiple models for PR review when complexity warrants it. Different models catch different classes of bugs.

### Test Matrices

- **Test across ALL config permutations.** A fix for one mode can break another. Build a test matrix covering every mode x every failure scenario.
- File bugs with full test matrices: reproduction steps, all root causes, gap matrix showing which PRs fix which scenarios.

### Issue & PR Workflow

- **Search existing issues before filing.** `gh issue list --search "<keywords>"` surfaces prior analysis.
- **Scope PRs to one logical change when possible.** Separate PRs are easier to review, revert, and bisect.
- **Call out behavior-default shifts explicitly in PR descriptions.** Include "old assumption vs new behavior" notes.

### Documentation Guardrails

- **Run doc checks before push, not after CI fails.** `pnpm check:docs`, `pnpm format:check`, `npx markdownlint-cli2`.
- **Verify command names against source, never memory.** Confirm in `package.json`, `CONTRIBUTING.md`, and `.github/workflows/ci.yml`.
- **Keep comments shell-safe when posting with `gh pr comment`.** Prefer single-quoted heredoc or escaped backticks.
- **After conflict resolution, run type checks.** Merge conflict fixes can drop `import type` lines.
- **Apply style rules to ALL locale variants.** Rules apply to `docs/zh-CN`, `docs/ja-JP`, etc.
- **Edit i18n docs via pipeline, not directly.** `docs/zh-CN/**` is generated by `scripts/docs-i18n`.

---

## 7. Debugging & Triage Workflow

1. Reproduce once with minimal scope.
2. Map symptom to module using the architecture map.
3. Check relevant gotchas in the current context document.
4. Run targeted tests first, then full suite if touching CRITICAL/HIGH-blast modules.
5. Confirm fix did not regress fallback paths (cron, channel send, subagent completion).

For ambiguous runtime failures: capture exact error string, check common errors table, then inspect nearest owning module.

### Common Errors -> First Fix

| Error / Symptom | First Check | First Fix |
| --- | --- | --- |
| `SQLITE_CANTOPEN` in daemon mode | LaunchAgent env (`TMPDIR`) | Reinstall/restart gateway service |
| `config.patch` "ok" but behavior unchanged | Wrong nesting path | Patch full nested key + read-back verify |
| Startup fails with token conflict | `hooks.token` equals `gateway.auth.token` | Set different values; restart gateway |
| SSRF block on webhook/browser URL | Target is private/metadata | Use public HTTPS endpoint |
| `await-thenable` lint error | Function is not async | Fix signature or remove incorrect `await` |

---

## 8. Pre-PR Checklist (Static Items)

> For the canonical command source, always check `CONTRIBUTING.md` + `.github/workflows/ci.yml`.

```
[] pnpm build                        # TypeScript compilation (always)
[] pnpm check                        # Format + type check + lint (always)
[] pnpm test                         # Full suite for high/blast changes
[] pnpm check:docs                   # Required when docs files changed
[] git diff --stat                   # Review staged scope
[] grep all callers                  # If changing exported signatures
[] Squash fix-on-fix commits         # Keep logical commits only

Conditional checks:
[] If `config/*` changed: run config-focused tests + read-back validation
[] If `routing/*` changed: verify session key parsing + cron + subagent routing
[] If `agents/pi-tools*` changed: run tool policy + tool execution paths
[] If `auto-reply/reply/get-reply.ts` changed: run reply pipeline checks across fallback paths
```

```

### references/VISION-GUIDELINES.md

```markdown
# Vision Guidelines

> Project direction, contribution policy, and merge guardrails from OpenClaw's VISION.md.
> PRs should be evaluated against these guidelines to catch scope, policy, and architectural misalignment early.

---

## 1. Contribution Rules

1. **One PR = one issue/topic.** Do not bundle multiple unrelated fixes/features.
2. **PRs over ~5,000 changed lines** are reviewed only in exceptional circumstances.
3. **Do not open large batches of tiny PRs at once;** each PR has review cost.
4. **For very small related fixes,** grouping into one focused PR is encouraged.

### Red Flags

| Signal | Severity | What to flag |
| --- | --- | --- |
| PR touches multiple unrelated modules/issues | πŸ”΄ | Suggest splitting into separate PRs |
| Diff exceeds ~5,000 lines | πŸ”΄ | Flag as likely too large for review |
| PR mixes feature + refactor + bugfix | 🟑 | Recommend separating concerns |
| Trivial fix that could be grouped with related trivials | 🟒 | Suggest grouping if other small PRs exist |

---

## 2. Security Philosophy

Security is a deliberate tradeoff: **strong defaults without killing capability.**

- Prioritize secure defaults.
- Expose clear knobs for trusted high-power workflows.
- Risky paths must be explicit and operator-controlled.

### Red Flags

| Signal | Severity | What to flag |
| --- | --- | --- |
| New capability enabled by default without opt-in | πŸ”΄ | Should default to off; operator enables |
| Security knob removed or hidden | πŸ”΄ | Operator must retain explicit control |
| New external integration without SSRF/auth consideration | 🟑 | Verify security surface |

---

## 3. Plugin & Core Boundary

Core stays lean; optional capability should ship as plugins.

- Preferred plugin path: npm package distribution + local extension loading for dev.
- Plugins should be hosted/maintained in their own repository.
- **The bar for adding optional plugins to core is intentionally high.**

### Red Flags

| Signal | Severity | What to flag |
| --- | --- | --- |
| New optional capability added directly to core | πŸ”΄ | Should be a plugin unless strong product/security reason |
| Plugin added to core repo instead of separate repo | 🟑 | Recommend external hosting |

---

## 4. Skills Policy

- Bundled skills exist for baseline UX only.
- **New skills should be published to ClawHub first**, not added to core.
- Core skill additions should be rare and require strong product or security reason.

### Red Flags

| Signal | Severity | What to flag |
| --- | --- | --- |
| New skill added to core without clear product/security justification | πŸ”΄ | Recommend ClawHub publication instead |

---

## 5. MCP Support

OpenClaw supports MCP through `mcporter` (external bridge).

- MCP integration stays decoupled from core runtime.
- Prefer the bridge model over first-class MCP runtime in core.

### Red Flags

| Signal | Severity | What to flag |
| --- | --- | --- |
| First-class MCP runtime added to core | πŸ”΄ | Should use mcporter bridge instead |
| MCP server management logic in core | 🟑 | Verify it doesn't duplicate mcporter |

---

## 6. Setup & Onboarding

- Terminal-first by design β€” keeps setup explicit.
- Users must see docs, auth, permissions, and security posture up front.
- **Do not add convenience wrappers that hide critical security decisions.**

### Red Flags

| Signal | Severity | What to flag |
| --- | --- | --- |
| Setup wrapper that auto-accepts security defaults | πŸ”΄ | User must explicitly see and decide |
| Auth/permission step hidden behind automation | 🟑 | Should remain visible and explicit |

---

## 7. What Will Not Be Merged (Current Policy)

These are explicit guardrails β€” PRs matching these patterns will not be merged:

1. **New core skills** when they can live on ClawHub.
2. **Full-doc translation sets** β€” deferred; AI-generated translations planned.
3. **Commercial service integrations** that don't fit model-provider category.
4. **Wrapper channels** around already-supported channels without clear capability/security gap.
5. **First-class MCP runtime** in core when mcporter provides the path.
6. **Agent-hierarchy frameworks** (manager-of-managers / nested planner trees) as default architecture.
7. **Heavy orchestration layers** that duplicate existing agent and tool infrastructure.

### Red Flags

| Signal | Severity | What to flag |
| --- | --- | --- |
| PR matches any "will not merge" category | πŸ”΄ | Flag with specific category match |
| PR adds i18n doc translations directly | πŸ”΄ | Use scripts/docs-i18n pipeline |
| PR adds nested agent orchestration as default | πŸ”΄ | Against architectural direction |

---

## 8. Project Priorities (Current Focus)

When evaluating whether a PR aligns with current priorities:

**Priority (highest):**
- Security and safe defaults
- Bug fixes and stability
- Setup reliability and first-run UX

**Next priorities:**
- Supporting all major model providers
- Improving major messaging channels
- Performance and test infrastructure
- Better computer-use and agent harness capabilities
- CLI and web frontend ergonomics
- Companion apps (macOS, iOS, Android, Windows, Linux)

### Scoring Guidance

- PRs aligned with top priorities get lower risk scores for scope.
- PRs that conflict with priorities or "will not merge" list get higher scores.
- This is informational context, not a gate β€” a well-executed PR in a lower-priority area is still fine.

```

### scripts/test-update-pipeline.sh

```bash
#!/usr/bin/env bash
# test-update-pipeline.sh β€” automated validation for pr-ship-update cron pipeline
# Run: bash scripts/test-update-pipeline.sh [--live]
# --live: also runs the actual cron job at the end (optional)
set -uo pipefail

SKILL_DIR="/home/dev/.openclaw/skills/pr-ship"
OPENCLAW_DIR="/home/dev/openclaw"
PASS=0
FAIL=0
WARN=0

green()  { printf "\033[32m%s\033[0m\n" "$*"; }
red()    { printf "\033[31m%s\033[0m\n" "$*"; }
yellow() { printf "\033[33m%s\033[0m\n" "$*"; }
bold()   { printf "\033[1m%s\033[0m\n" "$*"; }

pass() { ((PASS++)); green "  βœ” $1"; }
fail() { ((FAIL++)); red   "  ✘ $1"; }
warn() { ((WARN++)); yellow "  ⚠ $1"; }

# ─── Pre-flight ───────────────────────────────────────────────
bold "═══ pr-ship update pipeline tests ═══"
echo ""

# ─── T1: Git repo health ─────────────────────────────────────
bold "T1: Git repo health"

if [ -d "$SKILL_DIR/.git" ]; then
  pass ".git directory exists"
else
  fail ".git directory missing β€” run 'git init' in $SKILL_DIR"
fi

REMOTE=$(cd "$SKILL_DIR" && git remote get-url origin 2>/dev/null || echo "none")
if [[ "$REMOTE" == *"Glucksberg/pr-ship"* ]]; then
  pass "Remote origin points to Glucksberg/pr-ship"
else
  fail "Remote origin is '$REMOTE' β€” expected Glucksberg/pr-ship"
fi

BRANCH=$(cd "$SKILL_DIR" && git branch --show-current 2>/dev/null || echo "none")
if [ "$BRANCH" = "main" ]; then
  pass "On branch main"
else
  fail "On branch '$BRANCH' β€” expected main"
fi

echo ""

# ─── T2: Stray file leak prevention ──────────────────────────
bold "T2: Stray file leak prevention (defense-in-depth)"

cd "$SKILL_DIR"

# Test .gitignore blocks .env files
TRAP_FILE="$SKILL_DIR/.env.test-trap"
echo "SECRET_KEY=do-not-commit" > "$TRAP_FILE"
if git check-ignore -q "$TRAP_FILE" 2>/dev/null; then
  pass ".gitignore blocks .env files"
else
  fail ".gitignore does not block .env files"
fi
rm -f "$TRAP_FILE"

# Test .gitignore blocks swap files
SWAP_FILE="$SKILL_DIR/SKILL.md.swp"
touch "$SWAP_FILE"
if git check-ignore -q "$SWAP_FILE" 2>/dev/null; then
  pass ".gitignore blocks .swp files"
else
  warn ".gitignore does not block .swp files"
fi
rm -f "$SWAP_FILE"

# Test .gitignore blocks archive
if git check-ignore -q "$SKILL_DIR/references/DEVELOPER-REFERENCE.md.archive" 2>/dev/null; then
  pass ".gitignore blocks archive file"
else
  fail ".gitignore does not block archive file"
fi

echo ""

# ─── T3: Version bump logic ──────────────────────────────────
bold "T3: Version bump logic"

CURRENT_VER=$(jq -r .version "$SKILL_DIR/package.json")
EXPECTED_BUMP=$(node -e "const v='$CURRENT_VER'.split('.'); v[v.length-1]=+v[v.length-1]+1; console.log(v.join('.'))")

if [ -n "$EXPECTED_BUMP" ] && [ "$EXPECTED_BUMP" != "$CURRENT_VER" ]; then
  pass "Version bump: $CURRENT_VER β†’ $EXPECTED_BUMP"
else
  fail "Version bump produced invalid result: '$EXPECTED_BUMP'"
fi

# Test edge cases
for TEST_VER in "1.0.0" "1.0.99" "2.5.3" "0.0.1"; do
  RESULT=$(node -e "const v='$TEST_VER'.split('.'); v[v.length-1]=+v[v.length-1]+1; console.log(v.join('.'))")
  EXPECTED_LAST=$((${TEST_VER##*.} + 1))
  if [[ "$RESULT" == *".$EXPECTED_LAST" ]]; then
    pass "Bump $TEST_VER β†’ $RESULT"
  else
    fail "Bump $TEST_VER β†’ $RESULT (unexpected)"
  fi
done

echo ""

# ─── T4: sed metadata update ─────────────────────────────────
bold "T4: sed metadata update (dry-run on copy)"

TMPFILE=$(mktemp)
cp "$SKILL_DIR/references/CURRENT-CONTEXT.md" "$TMPFILE"

TEST_DATE="2099-01-01T00:00 UTC"
sed -i "s|_Auto-updated by.*|_Auto-updated by pr-ship-update cron. Last updated: ${TEST_DATE} (upstream sync)_|" "$TMPFILE"

if grep -q "pr-ship-update cron" "$TMPFILE"; then
  pass "Timestamp sed pattern matches and updates"
else
  fail "Timestamp sed pattern did NOT match β€” CURRENT-CONTEXT.md format may have changed"
fi

if grep -q "$TEST_DATE" "$TMPFILE"; then
  pass "Date injection works correctly"
else
  fail "Date was not injected into timestamp line"
fi

# Test version update
sed -i "s|## Active Version:.*|## Active Version: 9999.99.99 (Released)|" "$TMPFILE"
if grep -q "## Active Version: 9999.99.99" "$TMPFILE"; then
  pass "Version sed pattern matches and updates"
else
  fail "Version sed pattern did NOT match β€” check CURRENT-CONTEXT.md header format"
fi

rm -f "$TMPFILE"

echo ""

# ─── T5: CHANGELOG detection ─────────────────────────────────
bold "T5: CHANGELOG change detection"

cd "$OPENCLAW_DIR"
git fetch upstream --quiet 2>/dev/null || true
LOCAL_SHA=$(git rev-parse main:CHANGELOG.md 2>/dev/null || echo none)
UPSTREAM_SHA=$(git rev-parse upstream/main:CHANGELOG.md 2>/dev/null || echo none)

if [ "$LOCAL_SHA" != "none" ] && [ "$UPSTREAM_SHA" != "none" ]; then
  pass "Can read CHANGELOG SHA (local=${LOCAL_SHA:0:12} upstream=${UPSTREAM_SHA:0:12})"
  if [ "$LOCAL_SHA" = "$UPSTREAM_SHA" ]; then
    pass "CHANGELOGs match (cron would skip β€” this is normal)"
  else
    warn "CHANGELOGs differ (cron would trigger a metadata update)"
  fi
else
  fail "Cannot read CHANGELOG SHAs β€” git refs broken"
fi

# Version extraction
VER=$(git show upstream/main:CHANGELOG.md 2>/dev/null | grep -oP '^## \K[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "")
if [ -n "$VER" ]; then
  pass "Version extraction from CHANGELOG works: $VER"
else
  warn "Could not extract version from upstream CHANGELOG (may be empty or format changed)"
fi

echo ""

# ─── T6: Provenance β€” local vs GitHub HEAD ───────────────────
bold "T6: Provenance (local vs GitHub)"

cd "$SKILL_DIR"
LOCAL_SHA=$(git rev-parse --short HEAD 2>/dev/null || echo "none")
REMOTE_SHA=$(git ls-remote origin HEAD 2>/dev/null | cut -c1-7 || echo "none")

if [ "$LOCAL_SHA" != "none" ] && [ "$REMOTE_SHA" != "none" ]; then
  pass "Can query both local and remote HEAD"
  if [ "$LOCAL_SHA" = "$REMOTE_SHA" ]; then
    pass "In sync: local ($LOCAL_SHA) == GitHub ($REMOTE_SHA)"
  else
    warn "Out of sync: local=$LOCAL_SHA GitHub=$REMOTE_SHA β€” needs manual push"
  fi
else
  fail "Cannot query HEAD SHAs for provenance check"
fi

echo ""

# ─── T7: Cron job config validation ──────────────────────────
bold "T7: Cron job config"

JOBS_FILE="$HOME/.openclaw/cron/jobs.json"
if [ -f "$JOBS_FILE" ]; then
  JOB_MSG=$(python3 -c "
import json
jobs=json.load(open('$JOBS_FILE'))['jobs']
j=[x for x in jobs if x['id']=='492d067a-5cb1-47c5-92bc-fd8985c64a1f']
if j: print(j[0]['payload']['message'])
else: print('NOT_FOUND')
" 2>/dev/null || echo "PARSE_ERROR")

  if [ "$JOB_MSG" = "NOT_FOUND" ]; then
    fail "Cron job 492d067a not found in jobs.json"
  elif [ "$JOB_MSG" = "PARSE_ERROR" ]; then
    fail "Could not parse jobs.json"
  else
    pass "Cron job 492d067a found"

    # Cron should NOT have git push or clawhub publish
    if echo "$JOB_MSG" | grep -q "git push"; then
      fail "Cron still has git push β€” should be removed (manual sync only)"
    else
      pass "No git push in cron (manual sync only)"
    fi

    if echo "$JOB_MSG" | grep -q "clawhub publish"; then
      fail "Cron still has clawhub publish β€” should be removed (manual sync only)"
    else
      pass "No clawhub publish in cron (manual sync only)"
    fi

    # Should have metadata update + version bump
    if echo "$JOB_MSG" | grep -q "CURRENT-CONTEXT.md"; then
      pass "Updates CURRENT-CONTEXT.md metadata"
    else
      fail "Missing CURRENT-CONTEXT.md update step"
    fi

    if echo "$JOB_MSG" | grep -q "VERSION_BUMP"; then
      pass "Has version bump step"
    else
      fail "Missing version bump step"
    fi

    if echo "$JOB_MSG" | grep -q "pr-ship-update cron"; then
      pass "sed verification (grep -q) present"
    else
      warn "Missing sed verification after update"
    fi

    TIMEOUT=$(python3 -c "
import json
jobs=json.load(open('$JOBS_FILE'))['jobs']
j=[x for x in jobs if x['id']=='492d067a-5cb1-47c5-92bc-fd8985c64a1f'][0]
print(j['payload']['timeoutSeconds'])
" 2>/dev/null || echo "0")
    if [ "$TIMEOUT" -ge 60 ] && [ "$TIMEOUT" -le 120 ]; then
      pass "Timeout is ${TIMEOUT}s (appropriate for metadata-only job)"
    else
      warn "Timeout is ${TIMEOUT}s (expected 60-120s for metadata-only job)"
    fi
  fi
else
  fail "jobs.json not found at $JOBS_FILE"
fi

echo ""

# ─── T8: Skill file integrity ────────────────────────────────
bold "T8: Skill file integrity"

cd "$SKILL_DIR"

# package.json
if jq -e .homepage package.json &>/dev/null; then
  HP=$(jq -r .homepage package.json)
  if [[ "$HP" == *"github.com/Glucksberg/pr-ship"* ]]; then
    pass "package.json homepage present and correct"
  else
    fail "package.json homepage is '$HP' β€” expected Glucksberg/pr-ship URL"
  fi
else
  fail "package.json missing homepage field"
fi

if jq -e .bugs.url package.json &>/dev/null; then
  pass "package.json bugs.url present"
else
  fail "package.json missing bugs.url field"
fi

# SKILL.md
if grep -q "## Provenance" SKILL.md; then
  pass "SKILL.md has Provenance section"
else
  fail "SKILL.md missing Provenance section"
fi

if grep -q "## Security Notice" SKILL.md; then
  pass "SKILL.md has Security Notice section"
else
  fail "SKILL.md missing Security Notice section"
fi

# Provenance should mention manual verification, not auto-publish
if grep -q "GitHub repo is updated separately" SKILL.md; then
  pass "SKILL.md provenance says manual sync (no auto-publish)"
else
  warn "SKILL.md provenance may still reference auto-publish"
fi

# Should have diff verification commands for users
if grep -q "diff.*raw.githubusercontent.com" SKILL.md; then
  pass "SKILL.md has user verification commands"
else
  warn "SKILL.md missing user verification commands"
fi

# README.md
if [ -f README.md ]; then
  pass "README.md exists"
else
  fail "README.md missing"
fi

# .gitignore
if [ -f .gitignore ]; then
  if grep -q "DEVELOPER-REFERENCE.md.archive" .gitignore; then
    pass ".gitignore excludes archive"
  else
    fail ".gitignore missing archive exclusion"
  fi
  if grep -q ".env" .gitignore; then
    pass ".gitignore excludes .env files"
  else
    warn ".gitignore missing .env exclusion"
  fi
else
  fail ".gitignore missing"
fi

echo ""

# ─── T9: GitHub repo state ───────────────────────────────────
bold "T9: GitHub repo state"

if gh repo view Glucksberg/pr-ship &>/dev/null; then
  pass "GitHub repo Glucksberg/pr-ship exists"
else
  fail "GitHub repo Glucksberg/pr-ship not accessible"
fi

GH_FILES=$(gh api repos/Glucksberg/pr-ship/contents/ --jq '.[].name' 2>/dev/null | tr '\n' ' ')
for EXPECTED in .gitignore README.md SKILL.md package.json references; do
  if echo "$GH_FILES" | grep -q "$EXPECTED"; then
    pass "GitHub has $EXPECTED"
  else
    fail "GitHub missing $EXPECTED"
  fi
done

echo ""

# ─── Summary ─────────────────────────────────────────────────
bold "═══ Results ═══"
echo ""
green "  Passed:   $PASS"
if [ "$WARN" -gt 0 ]; then
  yellow "  Warnings: $WARN"
fi
if [ "$FAIL" -gt 0 ]; then
  red "  Failed:   $FAIL"
fi
echo ""

if [ "$FAIL" -gt 0 ]; then
  red "RESULT: $FAIL test(s) failed β€” fix before next cron run"
  EXIT_CODE=1
else
  if [ "$WARN" -gt 0 ]; then
    yellow "RESULT: All passed with $WARN warning(s)"
  else
    green "RESULT: All tests passed βœ”"
  fi
  EXIT_CODE=0
fi

# ─── Optional: live cron trigger ──────────────────────────────
if [[ "${1:-}" == "--live" ]]; then
  echo ""
  bold "═══ Live cron trigger ═══"
  echo "Triggering pr-ship-update cron job..."
  pnpm --dir "$OPENCLAW_DIR" openclaw cron run 492d067a-5cb1-47c5-92bc-fd8985c64a1f 2>&1 || true
  echo ""
  echo "Check Telegram for the report."
  echo "After manual sync, verify with:"
  echo "  cd $SKILL_DIR && git log --oneline -3"
  echo "  gh api repos/Glucksberg/pr-ship/commits/main --jq '.sha[:7]'"
fi

exit $EXIT_CODE

```