Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 498bb6de19 | |||
| 08a67c4a2d | |||
| c86a0673ba | |||
| cabf99ba07 | |||
| 2d978a6b64 | |||
| 4dbc9266c1 | |||
| ea0b3c8c8c | |||
| 0c56834385 | |||
| caccf0035e | |||
| 627b160f55 | |||
| 6463bc84b0 | |||
| f84f1229af | |||
| f85d21097b |
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -43,4 +43,7 @@ credentials.json
|
||||
lcov.info
|
||||
|
||||
# IDE's stuff
|
||||
.idea
|
||||
.idea
|
||||
|
||||
# Wrangler cache
|
||||
.wrangler/
|
||||
Generated
+1
-1
@@ -7945,7 +7945,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zeroclawlabs"
|
||||
version = "0.3.1"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-imap",
|
||||
|
||||
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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. 🦀
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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. 🦀
|
||||
|
||||
|
||||
Executable
+261
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user