Compare commits

...

13 Commits

Author SHA1 Message Date
argenis de la rosa 498bb6de19 fix: only tweet for stable releases, not beta builds
Remove tweet job from beta workflow so beta pushes no longer post to X.
Update tweet-release to diff features against last stable tag (not last
beta), so stable release tweets show ALL accumulated features across the
full beta cycle. Update tweet format to match desired feature-focused style.
2026-03-15 09:20:28 -04:00
Argenis 08a67c4a2d chore: bump version to v0.3.2 (#3564)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 06:47:37 -04:00
Argenis c86a0673ba feat(heartbeat): two-phase execution, structured tasks, and auto-routing (#3562)
Upgrade heartbeat system with 4 key improvements:

- Two-phase heartbeat: Phase 1 asks LLM "skip or run?" to save API cost
  on quiet periods. Phase 2 executes only selected tasks.
- Structured task format: `- [priority|status] task text` with
  high/medium/low priority and active/paused/completed status.
- Decision intelligence: LLM-driven smart filtering via structured prompt
  at temperature 0.0 for deterministic decisions.
- Delivery routing: auto-detect best configured channel when no explicit
  target is set (telegram > discord > slack > mattermost).

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 06:11:59 -04:00
Argenis cabf99ba07 Merge pull request #3539 from zeroclaw-labs/cleanup
chore: add .wrangler/ to gitignore
2026-03-14 22:53:12 -04:00
argenis de la rosa 2d978a6b64 chore: add .wrangler/ to gitignore and clean up stale files
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 22:41:17 -04:00
Argenis 4dbc9266c1 Merge pull request #3536 from zeroclaw-labs/test/termux-release-validation
test: add Termux release validation script
2026-03-14 22:21:21 -04:00
argenis de la rosa ea0b3c8c8c test: add Termux release validation script
Validates the aarch64-linux-android release artifact: download, archive
integrity, ELF format, architecture, checksum, and install.sh detection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 22:10:33 -04:00
Argenis 0c56834385 chore(ci): remove sync-readme workflow and script (#3535)
The auto-generated What's New and Recent Contributors README sections
have been removed, so the sync-readme workflow and its backing script
are no longer needed.
2026-03-14 21:38:04 -04:00
Argenis caccf0035e docs(readme): remove stale What's New and Contributors sections (#3534)
Clear auto-generated sections so they repopulate correctly on the next
release via sync-readme workflow.
2026-03-14 21:36:26 -04:00
Argenis 627b160f55 fix(install): clean up stale cargo tracking from old package name (#3532)
When the crate was renamed from `zeroclaw` to `zeroclawlabs`, users who
installed via install.sh (which uses `cargo install --path`) had the
binary tracked under the old package name. Later running
`cargo install zeroclawlabs` from crates.io fails with:

  error: binary `zeroclaw` already exists as part of `zeroclaw v0.1.9`

The installer now detects and removes stale tracking for the old
`zeroclaw` package name before installing, so both install.sh and
`cargo install zeroclawlabs` work cleanly for upgrades.
2026-03-14 21:20:02 -04:00
Argenis 6463bc84b0 fix(ci): sync tweet and README after all release artifacts are published (#3531)
The tweet was firing on `release: published` immediately when the GitHub
Release was created, but Docker push, crates.io publish, and website
redeploy were still in-flight. This meant users saw the tweet before
they could actually pull the new Docker image or install from crates.io.

Convert tweet-release.yml and sync-readme.yml to reusable workflows
(workflow_call) and call them as the final jobs in both release
pipelines, gated on completion of docker, crates-io, and
redeploy-website jobs.

Before: gh release create → tweet fires immediately (race condition)
After:  gh release create → docker + crates + website → tweet + readme
2026-03-14 21:19:56 -04:00
ZeroClaw Bot f84f1229af docs(readme): auto-sync What's New and Contributors 2026-03-15 01:07:01 +00:00
ZeroClaw Bot f85d21097b docs(readme): auto-sync What's New and Contributors 2026-03-15 01:01:55 +00:00
44 changed files with 1065 additions and 437 deletions
-139
View File
@@ -1,139 +0,0 @@
#!/usr/bin/env bash
# sync-readme.sh — Auto-update "What's New" and "Recent Contributors" in all READMEs
# Called by the sync-readme GitHub Actions workflow on each release.
set -euo pipefail
# --- Resolve version and ranges ---
LATEST_TAG=$(git tag --sort=-creatordate | head -1 || echo "")
if [ -z "$LATEST_TAG" ]; then
echo "No tags found — skipping README sync"
exit 0
fi
VERSION="${LATEST_TAG#v}"
# Find previous stable tag for contributor range
PREV_STABLE=$(git tag --sort=-creatordate \
| grep -v "^${LATEST_TAG}$" \
| grep -vE '\-beta\.' \
| head -1 || echo "")
FEAT_RANGE="${PREV_STABLE:+${PREV_STABLE}..}${LATEST_TAG}"
CONTRIB_RANGE="${PREV_STABLE:+${PREV_STABLE}..}${LATEST_TAG}"
# --- Build "What's New" table rows ---
FEATURES=$(git log "$FEAT_RANGE" --pretty=format:"%s" --no-merges \
| grep -iE '^feat(\(|:)' \
| sed 's/^feat(\([^)]*\)): /| \1 | /' \
| sed 's/^feat: /| General | /' \
| sed 's/ (#[0-9]*)$//' \
| sort -uf \
| while IFS= read -r line; do echo "${line} |"; done || true)
if [ -z "$FEATURES" ]; then
FEATURES="| General | Incremental improvements and polish |"
fi
MONTH_YEAR=$(date -u +"%B %Y")
# --- Build contributor list ---
GIT_AUTHORS=$(git log "$CONTRIB_RANGE" --pretty=format:"%an" --no-merges | sort -uf || true)
CO_AUTHORS=$(git log "$CONTRIB_RANGE" --pretty=format:"%b" --no-merges \
| grep -ioE 'Co-Authored-By: *[^<]+' \
| sed 's/Co-Authored-By: *//i' \
| sed 's/ *$//' \
| sort -uf || true)
ALL_CONTRIBUTORS=$(printf "%s\n%s" "$GIT_AUTHORS" "$CO_AUTHORS" \
| sort -uf \
| grep -v '^$' \
| grep -viE '\[bot\]$|^dependabot|^github-actions|^copilot|^ZeroClaw Bot|^ZeroClaw Runner|^ZeroClaw Agent|^blacksmith' \
|| true)
CONTRIBUTOR_COUNT=$(echo "$ALL_CONTRIBUTORS" | grep -c . || echo "0")
CONTRIBUTOR_LIST=$(echo "$ALL_CONTRIBUTORS" \
| while IFS= read -r name; do
[ -z "$name" ] && continue
echo "- **${name}**"
done || true)
# --- Write temp files for section content ---
WHATS_NEW_FILE=$(mktemp)
cat > "$WHATS_NEW_FILE" <<WHATS_EOF
### 🚀 What's New in ${LATEST_TAG} (${MONTH_YEAR})
| Area | Highlights |
|---|---|
${FEATURES}
WHATS_EOF
CONTRIBUTORS_FILE=$(mktemp)
cat > "$CONTRIBUTORS_FILE" <<CONTRIB_EOF
### 🌟 Recent Contributors (${LATEST_TAG})
${CONTRIBUTOR_COUNT} contributors shipped features, fixes, and improvements in this release cycle:
${CONTRIBUTOR_LIST}
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
CONTRIB_EOF
# --- Replace sections in all README files with markers ---
README_FILES=$(find . -maxdepth 1 -name 'README*.md' -type f | sort)
UPDATED=0
for readme in $README_FILES; do
if ! grep -q 'BEGIN:WHATS_NEW' "$readme"; then
continue
fi
python3 - "$readme" "$WHATS_NEW_FILE" "$CONTRIBUTORS_FILE" <<'PYEOF'
import sys, re
readme_path = sys.argv[1]
whats_new_path = sys.argv[2]
contributors_path = sys.argv[3]
with open(readme_path, 'r') as f:
content = f.read()
with open(whats_new_path, 'r') as f:
whats_new = f.read()
with open(contributors_path, 'r') as f:
contributors = f.read()
content = re.sub(
r'(<!-- BEGIN:WHATS_NEW -->)\n.*?(<!-- END:WHATS_NEW -->)',
r'\1\n' + whats_new + r'\2',
content,
flags=re.DOTALL
)
content = re.sub(
r'(<!-- BEGIN:RECENT_CONTRIBUTORS -->)\n.*?(<!-- END:RECENT_CONTRIBUTORS -->)',
r'\1\n' + contributors + r'\2',
content,
flags=re.DOTALL
)
with open(readme_path, 'w') as f:
f.write(content)
PYEOF
UPDATED=$((UPDATED + 1))
done
rm -f "$WHATS_NEW_FILE" "$CONTRIBUTORS_FILE"
echo "README synced: ${LATEST_TAG}${CONTRIBUTOR_COUNT} contributors — ${UPDATED} files updated"
@@ -313,3 +313,4 @@ jobs:
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
@@ -353,3 +353,13 @@ jobs:
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
# ── Post-publish: only run after ALL artifacts are live ──────────────
tweet:
name: Tweet Release
needs: [validate, publish, docker, crates-io, redeploy-website]
uses: ./.github/workflows/tweet-release.yml
with:
release_tag: ${{ needs.validate.outputs.tag }}
release_url: https://github.com/zeroclaw-labs/zeroclaw/releases/tag/${{ needs.validate.outputs.tag }}
secrets: inherit
-33
View File
@@ -1,33 +0,0 @@
name: Sync README
on:
release:
types: [published]
workflow_dispatch:
permissions:
contents: write
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
token: ${{ secrets.RELEASE_TOKEN }}
- name: Sync README sections
run: bash .github/scripts/sync-readme.sh
- name: Commit and push
run: |
git config user.name "ZeroClaw Bot"
git config user.email "bot@zeroclawlabs.ai"
if git diff --quiet -- 'README*.md'; then
echo "No README changes — skipping commit"
exit 0
fi
git add README*.md
git commit -m "docs(readme): auto-sync What's New and Contributors"
git push origin HEAD:master
+43 -40
View File
@@ -1,8 +1,26 @@
name: Tweet Release
on:
release:
types: [published]
# Called by release workflows AFTER all publish steps (docker, crates, website) complete.
workflow_call:
inputs:
release_tag:
description: "Stable release tag (e.g. v0.3.0)"
required: true
type: string
release_url:
description: "GitHub Release URL"
required: true
type: string
secrets:
TWITTER_CONSUMER_API_KEY:
required: false
TWITTER_CONSUMER_API_SECRET_KEY:
required: false
TWITTER_ACCESS_TOKEN:
required: false
TWITTER_ACCESS_TOKEN_SECRET:
required: false
workflow_dispatch:
inputs:
tweet_text:
@@ -26,7 +44,7 @@ jobs:
id: check
shell: bash
env:
RELEASE_TAG: ${{ github.event.release.tag_name || '' }}
RELEASE_TAG: ${{ inputs.release_tag || '' }}
MANUAL_TEXT: ${{ inputs.tweet_text || '' }}
run: |
# Manual dispatch always proceeds
@@ -35,9 +53,10 @@ jobs:
exit 0
fi
# Find the PREVIOUS release tag (including betas) to check for new features
# Find the previous STABLE release tag (exclude betas) to check for new features
PREV_TAG=$(git tag --sort=-creatordate \
| grep -v "^${RELEASE_TAG}$" \
| grep -vE '\-beta\.' \
| head -1 || echo "")
if [ -z "$PREV_TAG" ]; then
@@ -62,8 +81,8 @@ jobs:
if: steps.check.outputs.skip != 'true'
shell: bash
env:
RELEASE_TAG: ${{ github.event.release.tag_name || '' }}
RELEASE_URL: ${{ github.event.release.html_url || '' }}
RELEASE_TAG: ${{ inputs.release_tag || '' }}
RELEASE_URL: ${{ inputs.release_url || '' }}
MANUAL_TEXT: ${{ inputs.tweet_text || '' }}
run: |
set -euo pipefail
@@ -71,53 +90,37 @@ jobs:
if [ -n "$MANUAL_TEXT" ]; then
TWEET="$MANUAL_TEXT"
else
# For features: diff against the PREVIOUS release (including betas)
# This prevents duplicate feature lists across consecutive betas
PREV_RELEASE=$(git tag --sort=-creatordate \
| grep -v "^${RELEASE_TAG}$" \
| head -1 || echo "")
# For contributors: diff against the last STABLE release
# This captures everyone across the full release cycle
# Diff against the last STABLE release (exclude betas) to capture
# ALL features accumulated across the full beta cycle
PREV_STABLE=$(git tag --sort=-creatordate \
| grep -v "^${RELEASE_TAG}$" \
| grep -vE '\-beta\.' \
| head -1 || echo "")
FEAT_RANGE="${PREV_RELEASE:+${PREV_RELEASE}..}${RELEASE_TAG}"
CONTRIB_RANGE="${PREV_STABLE:+${PREV_STABLE}..}${RELEASE_TAG}"
RANGE="${PREV_STABLE:+${PREV_STABLE}..}${RELEASE_TAG}"
# Extract NEW features only since the last release
FEATURES=$(git log "$FEAT_RANGE" --pretty=format:"%s" --no-merges \
# Extract ALL features since the last stable release
FEATURES=$(git log "$RANGE" --pretty=format:"%s" --no-merges \
| grep -iE '^feat(\(|:)' \
| sed 's/^feat(\([^)]*\)): /\1: /' \
| sed 's/^feat: //' \
| sed 's/ (#[0-9]*)$//' \
| sort -uf \
| head -4 \
| while IFS= read -r line; do echo "🚀 ${line}"; done || true)
if [ -z "$FEATURES" ]; then
FEATURES="🚀 Incremental improvements and polish"
fi
# Count ALL contributors across the full release cycle
GIT_AUTHORS=$(git log "$CONTRIB_RANGE" --pretty=format:"%an" --no-merges | sort -uf || true)
CO_AUTHORS=$(git log "$CONTRIB_RANGE" --pretty=format:"%b" --no-merges \
| grep -ioE 'Co-Authored-By: *[^<]+' \
| sed 's/Co-Authored-By: *//i' \
| sed 's/ *$//' \
| sort -uf || true)
TOTAL_COUNT=$(printf "%s\n%s" "$GIT_AUTHORS" "$CO_AUTHORS" \
| sort -uf \
| grep -v '^$' \
| grep -viE '\[bot\]$|^dependabot|^github-actions|^copilot|^ZeroClaw Bot|^ZeroClaw Runner|^ZeroClaw Agent|^blacksmith' \
| grep -c . || echo "0")
FEAT_COUNT=$(echo "$FEATURES" | grep -c . || echo "0")
# Build tweet — new features, contributor count, hashtags
TWEET=$(printf "🦀 ZeroClaw %s\n\n%s\n\n🙌 %s contributors\n\n%s\n\n#zeroclaw #rust #ai #opensource" \
"$RELEASE_TAG" "$FEATURES" "$TOTAL_COUNT" "$RELEASE_URL")
# Format top features with rocket emoji (limit to 6 for tweet space)
FEAT_LIST=$(echo "$FEATURES" \
| head -6 \
| while IFS= read -r line; do echo "🚀 ${line}"; done || true)
if [ -z "$FEAT_LIST" ]; then
FEAT_LIST="🚀 Incremental improvements and polish"
fi
# Build tweet — feature-focused style
TWEET=$(printf "🦀 ZeroClaw %s\n\n%s\n\nZero overhead. Zero compromise. 100%% Rust.\n\n#zeroclaw #rust #ai #opensource" \
"$RELEASE_TAG" "$FEAT_LIST")
fi
# X/Twitter counts any URL as 23 chars (t.co shortening).
+4 -1
View File
@@ -43,4 +43,7 @@ credentials.json
lcov.info
# IDE's stuff
.idea
.idea
# Wrangler cache
.wrangler/
Generated
+1 -1
View File
@@ -7945,7 +7945,7 @@ dependencies = [
[[package]]
name = "zeroclawlabs"
version = "0.3.1"
version = "0.3.2"
dependencies = [
"anyhow",
"async-imap",
+1 -1
View File
@@ -4,7 +4,7 @@ resolver = "2"
[package]
name = "zeroclawlabs"
version = "0.3.1"
version = "0.3.2"
edition = "2021"
authors = ["theonlyhennygod"]
license = "MIT OR Apache-2.0"
+6 -4
View File
@@ -88,11 +88,11 @@
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -425,11 +425,13 @@ zeroclaw version # عرض الإصدار ومعلومات البنا
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -59,11 +59,11 @@
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -190,11 +190,13 @@ channels:
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -88,11 +88,11 @@ Postaveno studenty a členy komunit Harvard, MIT a Sundai.Club.
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -425,11 +425,13 @@ Stavíme v open source protože nejlepší nápady přicházejí odkudkoliv. Pok
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -59,11 +59,11 @@
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -190,11 +190,13 @@ Hvis ZeroClaw er nyttigt for dig, overvej venligst at købe os en kaffe:
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -92,11 +92,11 @@ Erstellt von Studenten und Mitgliedern der Harvard, MIT und Sundai.Club Gemeinsc
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -429,11 +429,13 @@ Wir bauen in Open Source, weil die besten Ideen von überall kommen. Wenn du das
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -56,11 +56,11 @@
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -189,11 +189,13 @@ channels:
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -88,11 +88,11 @@ Construido por estudiantes y miembros de las comunidades de Harvard, MIT y Sunda
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -425,11 +425,13 @@ Construimos en código abierto porque las mejores ideas vienen de todas partes.
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -59,11 +59,11 @@
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -190,11 +190,13 @@ Jos ZeroClaw on hyödyllinen sinulle, harkitse kahvin ostamista meille:
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -86,11 +86,11 @@ Construit par des étudiants et membres des communautés Harvard, MIT et Sundai.
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -423,11 +423,13 @@ Nous construisons en open source parce que les meilleures idées viennent de par
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -59,11 +59,11 @@
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -208,11 +208,13 @@ channels:
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -59,11 +59,11 @@
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -190,11 +190,13 @@ channels:
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -59,11 +59,11 @@
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -190,11 +190,13 @@ Ha a ZeroClaw hasznos az Ön számára, kérjük, fontolja meg, hogy vesz nekün
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -59,11 +59,11 @@
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -190,11 +190,13 @@ Jika ZeroClaw berguna bagi Anda, mohon pertimbangkan untuk membelikan kami kopi:
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -88,11 +88,11 @@ Costruito da studenti e membri delle comunità Harvard, MIT e Sundai.Club.
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -425,11 +425,13 @@ Costruiamo in open source perché le migliori idee vengono da ovunque. Se stai l
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -77,11 +77,11 @@
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -237,11 +237,13 @@ zeroclaw agent --provider anthropic -m "hello"
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -88,11 +88,11 @@ Harvard, MIT, 그리고 Sundai.Club 커뮤니티의 학생들과 멤버들이
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -425,11 +425,13 @@ ZeroClaw가 당신의 작업에 도움이 되었고 지속적인 개발을 지
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
-16
View File
@@ -85,13 +85,6 @@ Built by students and members of the Harvard, MIT, and Sundai.Club communities.
<p align="center"><code>Trait-driven architecture · secure-by-default runtime · provider/channel/tool swappable · pluggable everything</code></p>
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
<!-- END:WHATS_NEW -->
### 📢 Announcements
@@ -481,15 +474,6 @@ A heartfelt thank you to the communities and institutions that inspire and fuel
We're building in the open because the best ideas come from everywhere. If you're reading this, you're part of it. Welcome. 🦀❤️
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
1 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
<!-- END:RECENT_CONTRIBUTORS -->
## ⚠️ Official Repository & Impersonation Warning
+6 -4
View File
@@ -59,11 +59,11 @@
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -190,11 +190,13 @@ Hvis ZeroClaw er nyttig for deg, vennligst vurder å kjøpe oss en kaffe:
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -88,11 +88,11 @@ Gebouwd door studenten en leden van de Harvard, MIT en Sundai.Club gemeenschappe
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -425,11 +425,13 @@ We bouwen in open source omdat de beste ideeën van overal komen. Als je dit lee
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -88,11 +88,11 @@ Zbudowany przez studentów i członków społeczności Harvard, MIT i Sundai.Clu
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -425,11 +425,13 @@ Budujemy w open source ponieważ najlepsze pomysły przychodzą zewsząd. Jeśli
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -88,11 +88,11 @@ Construído por estudantes e membros das comunidades Harvard, MIT e Sundai.Club.
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -425,11 +425,13 @@ Construímos em código aberto porque as melhores ideias vêm de todo lugar. Se
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -59,11 +59,11 @@
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -190,11 +190,13 @@ Dacă ZeroClaw îți este util, te rugăm să iei în considerare să ne cumperi
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -77,11 +77,11 @@
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -237,11 +237,13 @@ zeroclaw agent --provider anthropic -m "hello"
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -59,11 +59,11 @@
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -190,11 +190,13 @@ Om ZeroClaw är användbart för dig, vänligen överväg att köpa en kaffe til
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -59,11 +59,11 @@
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -190,11 +190,13 @@ channels:
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -88,11 +88,11 @@ Binuo ng mga mag-aaral at miyembro ng Harvard, MIT, at Sundai.Club na komunidad.
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -425,11 +425,13 @@ Kami ay bumubuo sa open source dahil ang mga pinakamahusay na ideya ay nagmumula
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -88,11 +88,11 @@ Harvard, MIT ve Sundai.Club topluluklarının öğrencileri ve üyeleri tarafın
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -425,11 +425,13 @@ En iyi fikirler her yerden geldiği için açık kaynakta inşa ediyoruz. Bunu o
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -59,11 +59,11 @@
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -190,11 +190,13 @@ channels:
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -59,11 +59,11 @@
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -208,11 +208,13 @@ channels:
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -86,11 +86,11 @@
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -468,11 +468,13 @@ Chúng tôi xây dựng công khai vì ý tưởng hay đến từ khắp nơi.
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+6 -4
View File
@@ -77,11 +77,11 @@
<!-- BEGIN:WHATS_NEW -->
### 🚀 What's New in v0.3.0-beta.201 (March 2026)
### 🚀 What's New in v0.3.1 (March 2026)
| Area | Highlights |
|---|---|
| General | Incremental improvements and polish |
| ci | add Termux (aarch64-linux-android) release target |
<!-- END:WHATS_NEW -->
@@ -242,11 +242,13 @@ zeroclaw agent --provider anthropic -m "hello"
<!-- BEGIN:RECENT_CONTRIBUTORS -->
### 🌟 Recent Contributors (v0.3.0-beta.201)
### 🌟 Recent Contributors (v0.3.1)
1 contributors shipped features, fixes, and improvements in this release cycle:
3 contributors shipped features, fixes, and improvements in this release cycle:
- **Argenis**
- **argenis de la rosa**
- **Claude Opus 4.6**
Thank you to everyone who opened issues, reviewed PRs, translated docs, and helped test. Every contribution matters. 🦀
+261
View File
@@ -0,0 +1,261 @@
#!/usr/bin/env bash
# Termux release validation script
# Validates the aarch64-linux-android release artifact for Termux compatibility.
#
# Usage:
# ./dev/test-termux-release.sh [version]
#
# Examples:
# ./dev/test-termux-release.sh 0.3.1
# ./dev/test-termux-release.sh # auto-detects from Cargo.toml
#
set -euo pipefail
BLUE='\033[0;34m'
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[0;33m'
BOLD='\033[1m'
DIM='\033[2m'
RESET='\033[0m'
pass() { echo -e " ${GREEN}${RESET} $*"; }
fail() { echo -e " ${RED}${RESET} $*"; FAILURES=$((FAILURES + 1)); }
info() { echo -e "${BLUE}${RESET} ${BOLD}$*${RESET}"; }
warn() { echo -e "${YELLOW}!${RESET} $*"; }
FAILURES=0
TARGET="aarch64-linux-android"
VERSION="${1:-}"
if [[ -z "$VERSION" ]]; then
if [[ -f Cargo.toml ]]; then
VERSION=$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -1)
fi
fi
if [[ -z "$VERSION" ]]; then
echo "Usage: $0 <version>"
echo " e.g. $0 0.3.1"
exit 1
fi
TAG="v${VERSION}"
ASSET_NAME="zeroclaw-${TARGET}.tar.gz"
ASSET_URL="https://github.com/zeroclaw-labs/zeroclaw/releases/download/${TAG}/${ASSET_NAME}"
TEMP_DIR="$(mktemp -d -t zeroclaw-termux-test-XXXXXX)"
cleanup() { rm -rf "$TEMP_DIR"; }
trap cleanup EXIT
echo
echo -e "${BOLD}Termux Release Validation — ${TAG}${RESET}"
echo -e "${DIM}Target: ${TARGET}${RESET}"
echo
# --- Test 1: Release tag exists ---
info "Checking release tag ${TAG}"
if gh release view "$TAG" >/dev/null 2>&1; then
pass "Release ${TAG} exists"
else
fail "Release ${TAG} not found"
echo -e "${RED}Release has not been published yet. Wait for the release workflow to complete.${RESET}"
exit 1
fi
# --- Test 2: Android asset is listed ---
info "Checking for ${ASSET_NAME} in release assets"
ASSETS=$(gh release view "$TAG" --json assets -q '.assets[].name')
if echo "$ASSETS" | grep -q "$ASSET_NAME"; then
pass "Asset ${ASSET_NAME} found in release"
else
fail "Asset ${ASSET_NAME} not found in release"
echo "Available assets:"
echo "$ASSETS" | sed 's/^/ /'
exit 1
fi
# --- Test 3: Download the asset ---
info "Downloading ${ASSET_NAME}"
if curl -fsSL "$ASSET_URL" -o "$TEMP_DIR/$ASSET_NAME"; then
FILESIZE=$(wc -c < "$TEMP_DIR/$ASSET_NAME" | tr -d ' ')
pass "Downloaded successfully (${FILESIZE} bytes)"
else
fail "Download failed from ${ASSET_URL}"
exit 1
fi
# --- Test 4: Archive integrity ---
info "Verifying archive integrity"
if tar -tzf "$TEMP_DIR/$ASSET_NAME" >/dev/null 2>&1; then
pass "Archive is a valid gzip tar"
else
fail "Archive is corrupted or not a valid tar.gz"
exit 1
fi
# --- Test 5: Contains zeroclaw binary ---
info "Checking archive contents"
CONTENTS=$(tar -tzf "$TEMP_DIR/$ASSET_NAME")
if echo "$CONTENTS" | grep -q "^zeroclaw$"; then
pass "Archive contains 'zeroclaw' binary"
else
fail "Archive does not contain 'zeroclaw' binary"
echo "Contents:"
echo "$CONTENTS" | sed 's/^/ /'
fi
# --- Test 6: Extract and inspect binary ---
info "Extracting and inspecting binary"
tar -xzf "$TEMP_DIR/$ASSET_NAME" -C "$TEMP_DIR"
BINARY="$TEMP_DIR/zeroclaw"
if [[ -f "$BINARY" ]]; then
pass "Binary extracted"
else
fail "Binary not found after extraction"
exit 1
fi
# --- Test 7: ELF format and architecture ---
info "Checking binary format"
FILE_INFO=$(file "$BINARY")
if echo "$FILE_INFO" | grep -q "ELF"; then
pass "Binary is ELF format"
else
fail "Binary is not ELF format: $FILE_INFO"
fi
if echo "$FILE_INFO" | grep -qi "aarch64\|ARM aarch64"; then
pass "Binary targets aarch64 architecture"
else
fail "Binary does not target aarch64: $FILE_INFO"
fi
if echo "$FILE_INFO" | grep -qi "android\|bionic"; then
pass "Binary is linked for Android/Bionic"
else
# Android binaries may not always show "android" in file output,
# check with readelf if available
if command -v readelf >/dev/null 2>&1; then
INTERP=$(readelf -l "$BINARY" 2>/dev/null | grep -o '/[^ ]*linker[^ ]*' || true)
if echo "$INTERP" | grep -qi "android\|bionic"; then
pass "Binary uses Android linker: $INTERP"
else
warn "Could not confirm Android linkage (interpreter: ${INTERP:-unknown})"
warn "file output: $FILE_INFO"
fi
else
warn "Could not confirm Android linkage (readelf not available)"
warn "file output: $FILE_INFO"
fi
fi
# --- Test 8: Binary is stripped ---
info "Checking binary optimization"
if echo "$FILE_INFO" | grep -q "stripped"; then
pass "Binary is stripped (release optimized)"
else
warn "Binary may not be stripped"
fi
# --- Test 9: Binary is not dynamically linked to glibc ---
info "Checking for glibc dependencies"
if command -v readelf >/dev/null 2>&1; then
NEEDED=$(readelf -d "$BINARY" 2>/dev/null | grep NEEDED || true)
if echo "$NEEDED" | grep -qi "libc\.so\.\|libpthread\|libdl"; then
# Check if it's glibc or bionic
if echo "$NEEDED" | grep -qi "libc\.so\.6"; then
fail "Binary links against glibc (libc.so.6) — will not work on Termux"
else
pass "Binary links against libc (likely Bionic)"
fi
else
pass "No glibc dependencies detected"
fi
else
warn "readelf not available — skipping dynamic library check"
fi
# --- Test 10: SHA256 checksum verification ---
info "Verifying SHA256 checksum"
CHECKSUMS_URL="https://github.com/zeroclaw-labs/zeroclaw/releases/download/${TAG}/SHA256SUMS"
if curl -fsSL "$CHECKSUMS_URL" -o "$TEMP_DIR/SHA256SUMS" 2>/dev/null; then
EXPECTED=$(grep "$ASSET_NAME" "$TEMP_DIR/SHA256SUMS" | awk '{print $1}')
if [[ -n "$EXPECTED" ]]; then
if command -v sha256sum >/dev/null 2>&1; then
ACTUAL=$(sha256sum "$TEMP_DIR/$ASSET_NAME" | awk '{print $1}')
elif command -v shasum >/dev/null 2>&1; then
ACTUAL=$(shasum -a 256 "$TEMP_DIR/$ASSET_NAME" | awk '{print $1}')
else
warn "No sha256sum or shasum available"
ACTUAL=""
fi
if [[ -n "$ACTUAL" && "$ACTUAL" == "$EXPECTED" ]]; then
pass "SHA256 checksum matches"
elif [[ -n "$ACTUAL" ]]; then
fail "SHA256 mismatch: expected=$EXPECTED actual=$ACTUAL"
fi
else
warn "No checksum entry for ${ASSET_NAME} in SHA256SUMS"
fi
else
warn "Could not download SHA256SUMS"
fi
# --- Test 11: install.sh Termux detection ---
info "Validating install.sh Termux detection"
INSTALL_SH="install.sh"
if [[ ! -f "$INSTALL_SH" ]]; then
INSTALL_SH="$(dirname "$0")/../install.sh"
fi
if [[ -f "$INSTALL_SH" ]]; then
if grep -q 'TERMUX_VERSION' "$INSTALL_SH"; then
pass "install.sh checks TERMUX_VERSION"
else
fail "install.sh does not check TERMUX_VERSION"
fi
if grep -q 'aarch64-linux-android' "$INSTALL_SH"; then
pass "install.sh maps to aarch64-linux-android target"
else
fail "install.sh does not map to aarch64-linux-android"
fi
# Simulate Termux detection (mock uname as Linux since we may run on macOS)
detect_result=$(
bash -c '
TERMUX_VERSION="0.118"
os="Linux"
arch="aarch64"
case "$os:$arch" in
Linux:aarch64|Linux:arm64)
if [[ -n "${TERMUX_VERSION:-}" || -d "/data/data/com.termux" ]]; then
echo "aarch64-linux-android"
else
echo "aarch64-unknown-linux-gnu"
fi
;;
esac
'
)
if [[ "$detect_result" == "aarch64-linux-android" ]]; then
pass "Termux detection returns correct target (simulated)"
else
fail "Termux detection returned: $detect_result (expected aarch64-linux-android)"
fi
else
warn "install.sh not found — skipping detection tests"
fi
# --- Summary ---
echo
if [[ "$FAILURES" -eq 0 ]]; then
echo -e "${GREEN}${BOLD}All tests passed!${RESET}"
echo -e "${DIM}The Termux release artifact for ${TAG} is valid.${RESET}"
else
echo -e "${RED}${BOLD}${FAILURES} test(s) failed.${RESET}"
exit 1
fi
+11
View File
@@ -1199,6 +1199,17 @@ fi
if [[ "$SKIP_INSTALL" == false ]]; then
step_dot "Installing zeroclaw to cargo bin"
# Clean up stale cargo install tracking from the old "zeroclaw" package name
# (renamed to "zeroclawlabs"). Without this, `cargo install zeroclawlabs` from
# crates.io fails with "binary already exists as part of `zeroclaw`".
if have_cmd cargo; then
if [[ -f "$HOME/.cargo/.crates.toml" ]] && grep -q '^"zeroclaw ' "$HOME/.cargo/.crates.toml" 2>/dev/null; then
step_dot "Removing stale cargo tracking for old 'zeroclaw' package name"
cargo uninstall zeroclaw 2>/dev/null || true
fi
fi
cargo install --path "$WORK_DIR" --force --locked
step_ok "ZeroClaw installed"
else
+14 -1
View File
@@ -2895,22 +2895,34 @@ pub struct HeartbeatConfig {
pub enabled: bool,
/// Interval in minutes between heartbeat pings. Default: `30`.
pub interval_minutes: u32,
/// Enable two-phase heartbeat: Phase 1 asks LLM whether to run, Phase 2
/// executes only when the LLM decides there is work to do. Saves API cost
/// during quiet periods. Default: `true`.
#[serde(default = "default_two_phase")]
pub two_phase: bool,
/// Optional fallback task text when `HEARTBEAT.md` has no task entries.
#[serde(default)]
pub message: Option<String>,
/// Optional delivery channel for heartbeat output (for example: `telegram`).
/// When omitted, auto-selects the first configured channel.
#[serde(default, alias = "channel")]
pub target: Option<String>,
/// Optional delivery recipient/chat identifier (required when `target` is set).
/// Optional delivery recipient/chat identifier (required when `target` is
/// explicitly set).
#[serde(default, alias = "recipient")]
pub to: Option<String>,
}
fn default_two_phase() -> bool {
true
}
impl Default for HeartbeatConfig {
fn default() -> Self {
Self {
enabled: false,
interval_minutes: 30,
two_phase: true,
message: None,
target: None,
to: None,
@@ -6217,6 +6229,7 @@ default_temperature = 0.7
heartbeat: HeartbeatConfig {
enabled: true,
interval_minutes: 15,
two_phase: true,
message: Some("Check London time".into()),
target: Some("telegram".into()),
to: Some("123456".into()),
+140 -58
View File
@@ -203,14 +203,17 @@ where
}
async fn run_heartbeat_worker(config: Config) -> Result<()> {
use crate::heartbeat::engine::HeartbeatEngine;
let observer: std::sync::Arc<dyn crate::observability::Observer> =
std::sync::Arc::from(crate::observability::create_observer(&config.observability));
let engine = crate::heartbeat::engine::HeartbeatEngine::new(
let engine = HeartbeatEngine::new(
config.heartbeat.clone(),
config.workspace_dir.clone(),
observer,
);
let delivery = heartbeat_delivery_target(&config)?;
let delivery = resolve_heartbeat_delivery(&config)?;
let two_phase = config.heartbeat.two_phase;
let interval_mins = config.heartbeat.interval_minutes.max(5);
let mut interval = tokio::time::interval(Duration::from_secs(u64::from(interval_mins) * 60));
@@ -218,14 +221,71 @@ async fn run_heartbeat_worker(config: Config) -> Result<()> {
loop {
interval.tick().await;
let file_tasks = engine.collect_tasks().await?;
let tasks = heartbeat_tasks_for_tick(file_tasks, config.heartbeat.message.as_deref());
// Collect runnable tasks (active only, sorted by priority)
let mut tasks = engine.collect_runnable_tasks().await?;
if tasks.is_empty() {
continue;
// Try fallback message
if let Some(fallback) = config
.heartbeat
.message
.as_deref()
.map(str::trim)
.filter(|m| !m.is_empty())
{
tasks.push(crate::heartbeat::engine::HeartbeatTask {
text: fallback.to_string(),
priority: crate::heartbeat::engine::TaskPriority::Medium,
status: crate::heartbeat::engine::TaskStatus::Active,
});
} else {
continue;
}
}
for task in tasks {
let prompt = format!("[Heartbeat Task] {task}");
// ── Phase 1: LLM decision (two-phase mode) ──────────────
let tasks_to_run = if two_phase {
let decision_prompt = HeartbeatEngine::build_decision_prompt(&tasks);
match crate::agent::run(
config.clone(),
Some(decision_prompt),
None,
None,
0.0, // Low temperature for deterministic decision
vec![],
false,
None,
)
.await
{
Ok(response) => {
let indices = HeartbeatEngine::parse_decision_response(&response, tasks.len());
if indices.is_empty() {
tracing::info!("💓 Heartbeat Phase 1: skip (nothing to do)");
crate::health::mark_component_ok("heartbeat");
continue;
}
tracing::info!(
"💓 Heartbeat Phase 1: run {} of {} tasks",
indices.len(),
tasks.len()
);
indices
.into_iter()
.filter_map(|i| tasks.get(i).cloned())
.collect()
}
Err(e) => {
tracing::warn!("💓 Heartbeat Phase 1 failed, running all tasks: {e}");
tasks
}
}
} else {
tasks
};
// ── Phase 2: Execute selected tasks ─────────────────────
for task in &tasks_to_run {
let prompt = format!("[Heartbeat Task | {}] {}", task.priority, task.text);
let temp = config.default_temperature;
match crate::agent::run(
config.clone(),
@@ -242,7 +302,7 @@ async fn run_heartbeat_worker(config: Config) -> Result<()> {
Ok(output) => {
crate::health::mark_component_ok("heartbeat");
let announcement = if output.trim().is_empty() {
"heartbeat task executed".to_string()
format!("💓 heartbeat task completed: {}", task.text)
} else {
output
};
@@ -272,22 +332,8 @@ async fn run_heartbeat_worker(config: Config) -> Result<()> {
}
}
fn heartbeat_tasks_for_tick(
file_tasks: Vec<String>,
fallback_message: Option<&str>,
) -> Vec<String> {
if !file_tasks.is_empty() {
return file_tasks;
}
fallback_message
.map(str::trim)
.filter(|message| !message.is_empty())
.map(|message| vec![message.to_string()])
.unwrap_or_default()
}
fn heartbeat_delivery_target(config: &Config) -> Result<Option<(String, String)>> {
/// Resolve delivery target: explicit config > auto-detect first configured channel.
fn resolve_heartbeat_delivery(config: &Config) -> Result<Option<(String, String)>> {
let channel = config
.heartbeat
.target
@@ -302,16 +348,45 @@ fn heartbeat_delivery_target(config: &Config) -> Result<Option<(String, String)>
.filter(|value| !value.is_empty());
match (channel, target) {
(None, None) => Ok(None),
(Some(_), None) => anyhow::bail!("heartbeat.to is required when heartbeat.target is set"),
(None, Some(_)) => anyhow::bail!("heartbeat.target is required when heartbeat.to is set"),
// Both explicitly set — validate and use.
(Some(channel), Some(target)) => {
validate_heartbeat_channel_config(config, channel)?;
Ok(Some((channel.to_string(), target.to_string())))
}
// Only one set — error.
(Some(_), None) => anyhow::bail!("heartbeat.to is required when heartbeat.target is set"),
(None, Some(_)) => anyhow::bail!("heartbeat.target is required when heartbeat.to is set"),
// Neither set — try auto-detect the first configured channel.
(None, None) => Ok(auto_detect_heartbeat_channel(config)),
}
}
/// Auto-detect the best channel for heartbeat delivery by checking which
/// channels are configured. Returns the first match in priority order.
fn auto_detect_heartbeat_channel(config: &Config) -> Option<(String, String)> {
// Priority order: telegram > discord > slack > mattermost
if let Some(tg) = &config.channels_config.telegram {
// Use the first allowed_user as target, or fall back to empty (broadcast)
let target = tg.allowed_users.first().cloned().unwrap_or_default();
if !target.is_empty() {
return Some(("telegram".to_string(), target));
}
}
if config.channels_config.discord.is_some() {
// Discord requires explicit target — can't auto-detect
return None;
}
if config.channels_config.slack.is_some() {
// Slack requires explicit target
return None;
}
if config.channels_config.mattermost.is_some() {
// Mattermost requires explicit target
return None;
}
None
}
fn validate_heartbeat_channel_config(config: &Config, channel: &str) -> Result<()> {
match channel.to_ascii_lowercase().as_str() {
"telegram" => {
@@ -487,75 +562,56 @@ mod tests {
}
#[test]
fn heartbeat_tasks_use_file_tasks_when_available() {
let tasks =
heartbeat_tasks_for_tick(vec!["From file".to_string()], Some("Fallback from config"));
assert_eq!(tasks, vec!["From file".to_string()]);
}
#[test]
fn heartbeat_tasks_fall_back_to_config_message() {
let tasks = heartbeat_tasks_for_tick(vec![], Some(" check london time "));
assert_eq!(tasks, vec!["check london time".to_string()]);
}
#[test]
fn heartbeat_tasks_ignore_empty_fallback_message() {
let tasks = heartbeat_tasks_for_tick(vec![], Some(" "));
assert!(tasks.is_empty());
}
#[test]
fn heartbeat_delivery_target_none_when_unset() {
fn resolve_delivery_none_when_unset() {
let config = Config::default();
let target = heartbeat_delivery_target(&config).unwrap();
let target = resolve_heartbeat_delivery(&config).unwrap();
assert!(target.is_none());
}
#[test]
fn heartbeat_delivery_target_requires_to_field() {
fn resolve_delivery_requires_to_field() {
let mut config = Config::default();
config.heartbeat.target = Some("telegram".into());
let err = heartbeat_delivery_target(&config).unwrap_err();
let err = resolve_heartbeat_delivery(&config).unwrap_err();
assert!(err
.to_string()
.contains("heartbeat.to is required when heartbeat.target is set"));
}
#[test]
fn heartbeat_delivery_target_requires_target_field() {
fn resolve_delivery_requires_target_field() {
let mut config = Config::default();
config.heartbeat.to = Some("123456".into());
let err = heartbeat_delivery_target(&config).unwrap_err();
let err = resolve_heartbeat_delivery(&config).unwrap_err();
assert!(err
.to_string()
.contains("heartbeat.target is required when heartbeat.to is set"));
}
#[test]
fn heartbeat_delivery_target_rejects_unsupported_channel() {
fn resolve_delivery_rejects_unsupported_channel() {
let mut config = Config::default();
config.heartbeat.target = Some("email".into());
config.heartbeat.to = Some("ops@example.com".into());
let err = heartbeat_delivery_target(&config).unwrap_err();
let err = resolve_heartbeat_delivery(&config).unwrap_err();
assert!(err
.to_string()
.contains("unsupported heartbeat.target channel"));
}
#[test]
fn heartbeat_delivery_target_requires_channel_configuration() {
fn resolve_delivery_requires_channel_configuration() {
let mut config = Config::default();
config.heartbeat.target = Some("telegram".into());
config.heartbeat.to = Some("123456".into());
let err = heartbeat_delivery_target(&config).unwrap_err();
let err = resolve_heartbeat_delivery(&config).unwrap_err();
assert!(err
.to_string()
.contains("channels_config.telegram is not configured"));
}
#[test]
fn heartbeat_delivery_target_accepts_telegram_configuration() {
fn resolve_delivery_accepts_telegram_configuration() {
let mut config = Config::default();
config.heartbeat.target = Some("telegram".into());
config.heartbeat.to = Some("123456".into());
@@ -568,7 +624,33 @@ mod tests {
mention_only: false,
});
let target = heartbeat_delivery_target(&config).unwrap();
let target = resolve_heartbeat_delivery(&config).unwrap();
assert_eq!(target, Some(("telegram".to_string(), "123456".to_string())));
}
#[test]
fn auto_detect_telegram_when_configured() {
let mut config = Config::default();
config.channels_config.telegram = Some(crate::config::TelegramConfig {
bot_token: "bot-token".into(),
allowed_users: vec!["user123".into()],
stream_mode: crate::config::StreamMode::default(),
draft_update_interval_ms: 1000,
interrupt_on_new_message: false,
mention_only: false,
});
let target = resolve_heartbeat_delivery(&config).unwrap();
assert_eq!(
target,
Some(("telegram".to_string(), "user123".to_string()))
);
}
#[test]
fn auto_detect_none_when_no_channels() {
let config = Config::default();
let target = auto_detect_heartbeat_channel(&config);
assert!(target.is_none());
}
}
+399 -27
View File
@@ -1,11 +1,75 @@
use crate::config::HeartbeatConfig;
use crate::observability::{Observer, ObserverEvent};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::Path;
use std::sync::Arc;
use tokio::time::{self, Duration};
use tracing::{info, warn};
// ── Structured task types ────────────────────────────────────────
/// Priority level for a heartbeat task.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TaskPriority {
Low,
Medium,
High,
}
impl fmt::Display for TaskPriority {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Low => write!(f, "low"),
Self::Medium => write!(f, "medium"),
Self::High => write!(f, "high"),
}
}
}
/// Status of a heartbeat task.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TaskStatus {
Active,
Paused,
Completed,
}
impl fmt::Display for TaskStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Active => write!(f, "active"),
Self::Paused => write!(f, "paused"),
Self::Completed => write!(f, "completed"),
}
}
}
/// A structured heartbeat task with priority and status metadata.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HeartbeatTask {
pub text: String,
pub priority: TaskPriority,
pub status: TaskStatus,
}
impl HeartbeatTask {
pub fn is_runnable(&self) -> bool {
self.status == TaskStatus::Active
}
}
impl fmt::Display for HeartbeatTask {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}] {}", self.priority, self.text)
}
}
// ── Engine ───────────────────────────────────────────────────────
/// Heartbeat engine — reads HEARTBEAT.md and executes tasks periodically
pub struct HeartbeatEngine {
config: HeartbeatConfig,
@@ -64,8 +128,8 @@ impl HeartbeatEngine {
Ok(self.collect_tasks().await?.len())
}
/// Read HEARTBEAT.md and return all parsed tasks.
pub async fn collect_tasks(&self) -> Result<Vec<String>> {
/// Read HEARTBEAT.md and return all parsed structured tasks.
pub async fn collect_tasks(&self) -> Result<Vec<HeartbeatTask>> {
let heartbeat_path = self.workspace_dir.join("HEARTBEAT.md");
if !heartbeat_path.exists() {
return Ok(Vec::new());
@@ -74,13 +138,145 @@ impl HeartbeatEngine {
Ok(Self::parse_tasks(&content))
}
/// Parse tasks from HEARTBEAT.md (lines starting with `- `)
fn parse_tasks(content: &str) -> Vec<String> {
/// Collect only runnable (active) tasks, sorted by priority (high first).
pub async fn collect_runnable_tasks(&self) -> Result<Vec<HeartbeatTask>> {
let mut tasks: Vec<HeartbeatTask> = self
.collect_tasks()
.await?
.into_iter()
.filter(HeartbeatTask::is_runnable)
.collect();
// Sort by priority descending (High > Medium > Low)
tasks.sort_by(|a, b| b.priority.cmp(&a.priority));
Ok(tasks)
}
/// Parse tasks from HEARTBEAT.md with structured metadata support.
///
/// Supports both legacy flat format and new structured format:
///
/// Legacy:
/// `- Check email` → medium priority, active status
///
/// Structured:
/// `- [high] Check email` → high priority, active
/// `- [low|paused] Review old PRs` → low priority, paused
/// `- [completed] Old task` → medium priority, completed
fn parse_tasks(content: &str) -> Vec<HeartbeatTask> {
content
.lines()
.filter_map(|line| {
let trimmed = line.trim();
trimmed.strip_prefix("- ").map(ToString::to_string)
let text = trimmed.strip_prefix("- ")?;
if text.is_empty() {
return None;
}
Some(Self::parse_task_line(text))
})
.collect()
}
/// Parse a single task line into a structured `HeartbeatTask`.
///
/// Format: `[priority|status] task text` or just `task text`.
fn parse_task_line(text: &str) -> HeartbeatTask {
if let Some(rest) = text.strip_prefix('[') {
if let Some((meta, task_text)) = rest.split_once(']') {
let task_text = task_text.trim();
if !task_text.is_empty() {
let (priority, status) = Self::parse_meta(meta);
return HeartbeatTask {
text: task_text.to_string(),
priority,
status,
};
}
}
}
// No metadata — default to medium/active
HeartbeatTask {
text: text.to_string(),
priority: TaskPriority::Medium,
status: TaskStatus::Active,
}
}
/// Parse metadata tags like `high`, `low|paused`, `completed`.
fn parse_meta(meta: &str) -> (TaskPriority, TaskStatus) {
let mut priority = TaskPriority::Medium;
let mut status = TaskStatus::Active;
for part in meta.split('|') {
match part.trim().to_ascii_lowercase().as_str() {
"high" => priority = TaskPriority::High,
"medium" | "med" => priority = TaskPriority::Medium,
"low" => priority = TaskPriority::Low,
"active" => status = TaskStatus::Active,
"paused" | "pause" => status = TaskStatus::Paused,
"completed" | "complete" | "done" => status = TaskStatus::Completed,
_ => {}
}
}
(priority, status)
}
/// Build the Phase 1 LLM decision prompt for two-phase heartbeat.
pub fn build_decision_prompt(tasks: &[HeartbeatTask]) -> String {
let mut prompt = String::from(
"You are a heartbeat scheduler. Review the following periodic tasks and decide \
whether any should be executed right now.\n\n\
Consider:\n\
- Task priority (high tasks are more urgent)\n\
- Whether the task is time-sensitive or can wait\n\
- Whether running the task now would provide value\n\n\
Tasks:\n",
);
for (i, task) in tasks.iter().enumerate() {
use std::fmt::Write;
let _ = writeln!(prompt, "{}. [{}] {}", i + 1, task.priority, task.text);
}
prompt.push_str(
"\nRespond with ONLY one of:\n\
- `run: 1,2,3` (comma-separated task numbers to execute)\n\
- `skip` (nothing needs to run right now)\n\n\
Be conservative — skip if tasks are routine and not time-sensitive.",
);
prompt
}
/// Parse the Phase 1 LLM decision response.
///
/// Returns indices of tasks to run, or empty vec if skipped.
pub fn parse_decision_response(response: &str, task_count: usize) -> Vec<usize> {
let trimmed = response.trim().to_ascii_lowercase();
if trimmed == "skip" || trimmed.starts_with("skip") {
return Vec::new();
}
// Look for "run: 1,2,3" pattern
let numbers_part = if let Some(after_run) = trimmed.strip_prefix("run:") {
after_run.trim()
} else if let Some(after_run) = trimmed.strip_prefix("run ") {
after_run.trim()
} else {
// Try to parse as bare numbers
trimmed.as_str()
};
numbers_part
.split(',')
.filter_map(|s| {
let n: usize = s.trim().parse().ok()?;
if n >= 1 && n <= task_count {
Some(n - 1) // Convert to 0-indexed
} else {
None
}
})
.collect()
}
@@ -93,10 +289,14 @@ impl HeartbeatEngine {
# Add tasks below (one per line, starting with `- `)\n\
# The agent will check this file on each heartbeat tick.\n\
#\n\
# Format: - [priority|status] Task description\n\
# priority: high, medium (default), low\n\
# status: active (default), paused, completed\n\
#\n\
# Examples:\n\
# - Check my email for important messages\n\
# - [high] Check my email for important messages\n\
# - Review my calendar for upcoming events\n\
# - Check the weather forecast\n";
# - [low|paused] Check the weather forecast\n";
tokio::fs::write(&path, default).await?;
}
Ok(())
@@ -112,9 +312,9 @@ mod tests {
let content = "# Tasks\n\n- Check email\n- Review calendar\nNot a task\n- Third task";
let tasks = HeartbeatEngine::parse_tasks(content);
assert_eq!(tasks.len(), 3);
assert_eq!(tasks[0], "Check email");
assert_eq!(tasks[1], "Review calendar");
assert_eq!(tasks[2], "Third task");
assert_eq!(tasks[0].text, "Check email");
assert_eq!(tasks[0].priority, TaskPriority::Medium);
assert_eq!(tasks[0].status, TaskStatus::Active);
}
#[test]
@@ -133,26 +333,21 @@ mod tests {
let content = " - Indented task\n\t- Tab indented";
let tasks = HeartbeatEngine::parse_tasks(content);
assert_eq!(tasks.len(), 2);
assert_eq!(tasks[0], "Indented task");
assert_eq!(tasks[1], "Tab indented");
assert_eq!(tasks[0].text, "Indented task");
assert_eq!(tasks[1].text, "Tab indented");
}
#[test]
fn parse_tasks_dash_without_space_ignored() {
let content = "- Real task\n-\n- Another";
let tasks = HeartbeatEngine::parse_tasks(content);
// "-" trimmed = "-", does NOT start with "- " => skipped
// "- Real task" => "Real task"
// "- Another" => "Another"
assert_eq!(tasks.len(), 2);
assert_eq!(tasks[0], "Real task");
assert_eq!(tasks[1], "Another");
assert_eq!(tasks[0].text, "Real task");
assert_eq!(tasks[1].text, "Another");
}
#[test]
fn parse_tasks_trailing_space_bullet_trimmed_to_dash() {
// "- " trimmed becomes "-" (trim removes trailing space)
// "-" does NOT start with "- " => skipped
let content = "- ";
let tasks = HeartbeatEngine::parse_tasks(content);
assert_eq!(tasks.len(), 0);
@@ -160,11 +355,10 @@ mod tests {
#[test]
fn parse_tasks_bullet_with_content_after_spaces() {
// "- hello " trimmed becomes "- hello" => starts_with "- " => "hello"
let content = "- hello ";
let tasks = HeartbeatEngine::parse_tasks(content);
assert_eq!(tasks.len(), 1);
assert_eq!(tasks[0], "hello");
assert_eq!(tasks[0].text, "hello");
}
#[test]
@@ -172,8 +366,8 @@ mod tests {
let content = "- Check email 📧\n- Review calendar 📅\n- 日本語タスク";
let tasks = HeartbeatEngine::parse_tasks(content);
assert_eq!(tasks.len(), 3);
assert!(tasks[0].contains("📧"));
assert!(tasks[2].contains("日本語"));
assert!(tasks[0].text.contains('📧'));
assert!(tasks[2].text.contains("日本語"));
}
#[test]
@@ -181,15 +375,15 @@ mod tests {
let content = "# Periodic Tasks\n\n## Quick\n- Task A\n\n## Long\n- Task B\n\n* Not a dash bullet\n1. Not numbered";
let tasks = HeartbeatEngine::parse_tasks(content);
assert_eq!(tasks.len(), 2);
assert_eq!(tasks[0], "Task A");
assert_eq!(tasks[1], "Task B");
assert_eq!(tasks[0].text, "Task A");
assert_eq!(tasks[1].text, "Task B");
}
#[test]
fn parse_tasks_single_task() {
let tasks = HeartbeatEngine::parse_tasks("- Only one");
assert_eq!(tasks.len(), 1);
assert_eq!(tasks[0], "Only one");
assert_eq!(tasks[0].text, "Only one");
}
#[test]
@@ -201,9 +395,153 @@ mod tests {
});
let tasks = HeartbeatEngine::parse_tasks(&content);
assert_eq!(tasks.len(), 100);
assert_eq!(tasks[99], "Task 99");
assert_eq!(tasks[99].text, "Task 99");
}
// ── Structured task parsing tests ────────────────────────────
#[test]
fn parse_task_with_high_priority() {
let content = "- [high] Urgent email check";
let tasks = HeartbeatEngine::parse_tasks(content);
assert_eq!(tasks.len(), 1);
assert_eq!(tasks[0].text, "Urgent email check");
assert_eq!(tasks[0].priority, TaskPriority::High);
assert_eq!(tasks[0].status, TaskStatus::Active);
}
#[test]
fn parse_task_with_low_paused() {
let content = "- [low|paused] Review old PRs";
let tasks = HeartbeatEngine::parse_tasks(content);
assert_eq!(tasks.len(), 1);
assert_eq!(tasks[0].text, "Review old PRs");
assert_eq!(tasks[0].priority, TaskPriority::Low);
assert_eq!(tasks[0].status, TaskStatus::Paused);
}
#[test]
fn parse_task_completed() {
let content = "- [completed] Old task";
let tasks = HeartbeatEngine::parse_tasks(content);
assert_eq!(tasks.len(), 1);
assert_eq!(tasks[0].priority, TaskPriority::Medium);
assert_eq!(tasks[0].status, TaskStatus::Completed);
}
#[test]
fn parse_task_without_metadata_defaults() {
let content = "- Plain task";
let tasks = HeartbeatEngine::parse_tasks(content);
assert_eq!(tasks.len(), 1);
assert_eq!(tasks[0].text, "Plain task");
assert_eq!(tasks[0].priority, TaskPriority::Medium);
assert_eq!(tasks[0].status, TaskStatus::Active);
}
#[test]
fn parse_mixed_structured_and_legacy() {
let content = "- [high] Urgent\n- Normal task\n- [low|paused] Later";
let tasks = HeartbeatEngine::parse_tasks(content);
assert_eq!(tasks.len(), 3);
assert_eq!(tasks[0].priority, TaskPriority::High);
assert_eq!(tasks[1].priority, TaskPriority::Medium);
assert_eq!(tasks[2].priority, TaskPriority::Low);
assert_eq!(tasks[2].status, TaskStatus::Paused);
}
#[test]
fn runnable_filters_paused_and_completed() {
let content = "- [high] Active\n- [low|paused] Paused\n- [completed] Done";
let tasks = HeartbeatEngine::parse_tasks(content);
let runnable: Vec<_> = tasks
.into_iter()
.filter(HeartbeatTask::is_runnable)
.collect();
assert_eq!(runnable.len(), 1);
assert_eq!(runnable[0].text, "Active");
}
// ── Two-phase decision tests ────────────────────────────────
#[test]
fn decision_prompt_includes_all_tasks() {
let tasks = vec![
HeartbeatTask {
text: "Check email".into(),
priority: TaskPriority::High,
status: TaskStatus::Active,
},
HeartbeatTask {
text: "Review calendar".into(),
priority: TaskPriority::Medium,
status: TaskStatus::Active,
},
];
let prompt = HeartbeatEngine::build_decision_prompt(&tasks);
assert!(prompt.contains("1. [high] Check email"));
assert!(prompt.contains("2. [medium] Review calendar"));
assert!(prompt.contains("skip"));
assert!(prompt.contains("run:"));
}
#[test]
fn parse_decision_skip() {
let indices = HeartbeatEngine::parse_decision_response("skip", 3);
assert!(indices.is_empty());
}
#[test]
fn parse_decision_skip_with_reason() {
let indices =
HeartbeatEngine::parse_decision_response("skip — nothing urgent right now", 3);
assert!(indices.is_empty());
}
#[test]
fn parse_decision_run_single() {
let indices = HeartbeatEngine::parse_decision_response("run: 1", 3);
assert_eq!(indices, vec![0]);
}
#[test]
fn parse_decision_run_multiple() {
let indices = HeartbeatEngine::parse_decision_response("run: 1, 3", 3);
assert_eq!(indices, vec![0, 2]);
}
#[test]
fn parse_decision_run_out_of_range_ignored() {
let indices = HeartbeatEngine::parse_decision_response("run: 1, 5, 2", 3);
assert_eq!(indices, vec![0, 1]);
}
#[test]
fn parse_decision_run_zero_ignored() {
let indices = HeartbeatEngine::parse_decision_response("run: 0, 1", 3);
assert_eq!(indices, vec![0]);
}
// ── Task display ────────────────────────────────────────────
#[test]
fn task_display_format() {
let task = HeartbeatTask {
text: "Check email".into(),
priority: TaskPriority::High,
status: TaskStatus::Active,
};
assert_eq!(format!("{task}"), "[high] Check email");
}
#[test]
fn priority_ordering() {
assert!(TaskPriority::High > TaskPriority::Medium);
assert!(TaskPriority::Medium > TaskPriority::Low);
}
// ── Async tests ─────────────────────────────────────────────
#[tokio::test]
async fn ensure_heartbeat_file_creates_file() {
let dir = std::env::temp_dir().join("zeroclaw_test_heartbeat");
@@ -216,6 +554,7 @@ mod tests {
assert!(path.exists());
let content = tokio::fs::read_to_string(&path).await.unwrap();
assert!(content.contains("Periodic Tasks"));
assert!(content.contains("[high]"));
let _ = tokio::fs::remove_dir_all(&dir).await;
}
@@ -301,4 +640,37 @@ mod tests {
let result = engine.run().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn collect_runnable_tasks_sorts_by_priority() {
let dir = std::env::temp_dir().join("zeroclaw_test_runnable_sort");
let _ = tokio::fs::remove_dir_all(&dir).await;
tokio::fs::create_dir_all(&dir).await.unwrap();
tokio::fs::write(
dir.join("HEARTBEAT.md"),
"- [low] Low task\n- [high] High task\n- Medium task\n- [low|paused] Skip me",
)
.await
.unwrap();
let observer: Arc<dyn Observer> = Arc::new(crate::observability::NoopObserver);
let engine = HeartbeatEngine::new(
HeartbeatConfig {
enabled: true,
interval_minutes: 30,
..HeartbeatConfig::default()
},
dir.clone(),
observer,
);
let tasks = engine.collect_runnable_tasks().await.unwrap();
assert_eq!(tasks.len(), 3); // paused one excluded
assert_eq!(tasks[0].priority, TaskPriority::High);
assert_eq!(tasks[1].priority, TaskPriority::Medium);
assert_eq!(tasks[2].priority, TaskPriority::Low);
let _ = tokio::fs::remove_dir_all(&dir).await;
}
}