Compare commits

...
Author SHA1 Message Date
Thomas SharedInboxandClaude Sonnet 4.6 3ef4ec3094 fix: guard against empty IMAP fetch message list (#339)
Replace unsafe .first on fetch.messages with .firstOrNull + early throw in
getEmailBody, downloadAttachment, and fetchRawRfc822, so a UID-expunged or
network-hiccup response gives a descriptive StateError instead of the
opaque "No element" crash. Also add snapshot.hasError handling in the
FutureBuilder so users see an error message rather than an infinite spinner.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 18:05:25 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 968db75c69 feat: replace agent_loop.py with agentloop
Switch from the bespoke 1136-line Python orchestrator to the community
agentloop tool (https://github.com/guettli/agentloop). The new tool
handles the issue → agent → PR pipeline via a label state machine using
loop/plan and loop/code labels, running every 5 minutes via cron.

Removes: scripts/agent_loop.py, scripts/test_agent_loop.py
Removes: .forgejo/workflows/monitor.yml (no heartbeat concept in agentloop)
Updates: AGENTS.md to document the new loop/ label workflow

agentloop config lives in ~/agentloop/loop/sharedinbox/ on the host.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 09:20:48 +02:00
Bot of Thomas Güttler d905cd653f fix: check Docker availability before falling back to local Dagger engine (#329) (#333) 2026-05-29 23:19:14 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 e21cde0a3c fix: allow forgejo-actions as issue author in agent loop
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 21:52:56 +02:00
Bot of Thomas Güttler 50a6678ec2 feat: reimplement user preferences, archive, configurable navigation (#315) (#324) 2026-05-29 19:08:12 +02:00
Bot of Thomas Güttler 91083218d4 fix: diff from last deployed SHA to catch all changes since last deploy (#320) (#332) 2026-05-29 17:34:21 +02:00
Bot of Thomas Güttler adc4eb6f6d feat: remove publish-website from deploy.yml, schedule website.yml hourly (#325) (#330) 2026-05-29 12:53:18 +02:00
12 changed files with 91 additions and 2251 deletions
+13 -48
View File
@@ -17,7 +17,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
fetch-depth: 0
- name: Detect Android and Linux changes
id: diff
@@ -48,7 +48,7 @@ jobs:
data = json.loads(r.read())
runs = [
r for r in data.get("workflow_runs", [])
if r.get("workflow_id") == "deploy.yml" and r.get("status") == "success"
if r.get("status") == "success"
]
print(runs[0].get("commit_sha") or "")
except Exception as e:
@@ -64,10 +64,17 @@ jobs:
exit 0
fi
# Diff the HEAD commit against its parent; fall back to listing HEAD's files
# when the parent is unavailable (initial commit, shallow clone).
CHANGED=$(git diff --name-only HEAD~1 HEAD 2>/dev/null \
|| git show --name-only --format= HEAD)
# Diff from the last successfully deployed commit to catch all changes since
# that deploy, not just the most recent commit. Falls back to HEAD~1 when
# LAST_DEPLOYED_SHA is unknown or not in local history.
if [ -n "$LAST_DEPLOYED_SHA" ] && git cat-file -e "$LAST_DEPLOYED_SHA" 2>/dev/null; then
echo "Diffing from last deployed SHA $LAST_DEPLOYED_SHA"
CHANGED=$(git diff --name-only "$LAST_DEPLOYED_SHA" HEAD 2>/dev/null \
|| git show --name-only --format= HEAD)
else
CHANGED=$(git diff --name-only HEAD~1 HEAD 2>/dev/null \
|| git show --name-only --format= HEAD)
fi
echo "Changed files:"
echo "$CHANGED"
@@ -204,48 +211,6 @@ jobs:
if: always()
run: rm -rf /tmp/dagger-tls /tmp/stunnel-dagger.conf /tmp/stunnel.pid
publish-website:
name: Publish Website Build History
runs-on: ubuntu-latest
needs: [build-linux, deploy-playstore, deploy-apk]
if: |
always() &&
(needs.build-linux.result == 'success' || needs.deploy-playstore.result == 'success' || needs.deploy-apk.result == 'success')
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Check runner tools
run: |
command -v dagger >/dev/null 2>&1 || { echo "ERROR: dagger is not installed in the runner image. Add it to .forgejo/Dockerfile."; exit 1; }
command -v task >/dev/null 2>&1 || { echo "ERROR: task is not installed in the runner image. Add it to .forgejo/Dockerfile."; exit 1; }
dpkg -s stunnel4 netcat-openbsd >/dev/null 2>&1 || { echo "ERROR: stunnel4/netcat-openbsd are not installed in the runner image. Add them to .forgejo/Dockerfile."; exit 1; }
- name: Setup Dagger Remote Engine (via stunnel)
env:
DAGGER_STUNNEL_URL: ${{ secrets.DAGGER_STUNNEL_URL }}
DAGGER_CA_CERT: ${{ secrets.DAGGER_CA_CERT }}
DAGGER_CLIENT_CERT: ${{ secrets.DAGGER_CLIENT_CERT }}
DAGGER_CLIENT_KEY: ${{ secrets.DAGGER_CLIENT_KEY }}
run: scripts/setup_dagger_remote.sh
- name: Generate build history and deploy website
if: ${{ secrets.SSH_PRIVATE_KEY != '' }}
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }}
SSH_USER: ${{ secrets.SSH_USER }}
SSH_HOST: ${{ secrets.SSH_HOST }}
DAGGER_NO_NAG: "1"
run: task publish-website
- name: Cleanup TLS credentials
if: always()
run: rm -rf /tmp/dagger-tls /tmp/stunnel-dagger.conf /tmp/stunnel.pid
label-deploy-health:
name: Update Deploy Health Label
runs-on: ubuntu-latest
-18
View File
@@ -1,18 +0,0 @@
name: Monitor Agent Loop
on:
schedule:
- cron: '0 */2 * * *' # every 2 hours
workflow_dispatch:
jobs:
monitor:
name: Check Agent Loop Health
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: Check agent loop heartbeat
run: python3 scripts/agent_loop.py monitor
+2
View File
@@ -1,6 +1,8 @@
name: Update Website
on:
schedule:
- cron: '0 * * * *' # every hour on the hour
push:
branches: [main]
paths:
+27 -32
View File
@@ -8,46 +8,41 @@ CLI tool `fgj` is available to query issues/PRs/actions.
## Issue Label Workflow
We use issues, follow this label state machine:
Automation is handled by [agentloop](https://github.com/guettli/agentloop) running every 5 minutes via cron. Add a label to trigger an agent:
- **State/ToPlan** — Issue needs a plan written by an agent before implementation
- **State/Planned** — Plan has been posted as a comment; awaiting human review
- **State/Ready** — Issue is approved and ready for implementation
- **State/InProgress** — Set while an agent (or human) is actively working
- **State/Question** — Agent hit a blocker or needs clarification
| Label | Trigger | Outcome |
|---|---|---|
| `loop/plan` | Planning agent reads the issue and writes an implementation plan as a comment | Issue moves to `loop/plan-done` |
| `loop/code` | Coding agent implements the change, creates a branch + PR | Issue moves to `loop/code-done` |
Full lifecycle:
**State machine:**
```
State/ToPlan → State/Planned (automated: agent_loop.py runs a planning agent)
State/Planned → State/Ready (manual: human reviews the plan and approves)
State/Ready → State/InProgress (automated: agent_loop.py before starting implementation)
State/InProgress → closed (automated: after PR is merged and CI passes)
any state → State/Question (automated or manual: when blocked)
loop/plan loop/plan-in-progress → loop/plan-done
↘ NeedSupervisor (on failure)
loop/code → loop/code-in-progress loop/code-done
NeedSupervisor (on failure)
```
List open issues ready to pick up:
**Rules:**
- Only issues authored by allowed users are picked up (guettli, guettlibot, guettlibot2, forgejo-actions).
- An issue with `NeedSupervisor` needs human attention — investigate, fix, then re-label.
- The coding agent opens a PR but does NOT close the issue. A human reviews the PR and closes the issue after merging.
- Planning agents only post a comment — they do NOT write code or open PRs.
- `loop/*` labels are managed by agentloop — do not set them manually while an agent is active.
**Typical lifecycle for a new feature:**
```bash
fgj issue list --json --state open | jq '[.[] | select(.labels[].name == "State/Ready")] | .[] | {number, title, html_url}'
```
Rules:
- Never start implementation on an issue without `State/Ready`
- Planning agents only post a plan comment — they do NOT write code or open PRs
- After `State/Planned`, a human must review the plan and manually add `State/Ready`
- When working via the agent loop: label transitions are set automatically
by `agent_loop.py` — do **not** set them yourself.
- When working manually: switch to `State/InProgress` as your **first action**:
```bash
fgj issue edit <NUMBER> --remove-label "State/Ready" --add-label "State/InProgress"
```
- If blocked, replace current state label with `State/Question` and leave a comment explaining the blocker
- When done and CI is green, close the issue:
```bash
fgj issue close <NUMBER>
```
1. Create issue
2. Add label loop/plan → agent writes plan as comment
3. Review plan, request changes or approve
4. Add label loop/code → agent implements + opens PR
5. Review PR, merge
6. Close issue
```
## Code conventions
+1 -1
View File
@@ -294,7 +294,7 @@ tasks:
for attempt in 1 2 3; do
run_dagger "$@" && return 0
RC=$?
if [ "$attempt" -lt 3 ] && grep -qE "connection reset|context canceled|connection refused|invalid return status code" "$DAGGER_OUT"; then
if [ "$attempt" -lt 3 ] && { grep -qE "connection reset|context canceled|context deadline exceeded|connection refused|invalid return status code" "$DAGGER_OUT" || [ "$RC" -eq 2 ]; }; then
echo "$(_ts) dagger: network error on attempt $attempt/3, retrying..." >&2
elif [ "$attempt" -lt 3 ] && grep -q "No space left on device" "$DAGGER_OUT"; then
echo "$(_ts) dagger: disk space error on attempt $attempt/3, pruning Dagger cache..." >&2
+12
View File
@@ -4,6 +4,18 @@ This file contains tasks which got implemented.
Tasks get moved from next.md to done.md
## Tasks (2026-05-29)
- **Merge PR #307 — user preferences and configurable navigation (Issue #315)**: Confirmed that
all features from PR #307 (issue #299) were already merged into main via separate PRs:
- Configurable menu bar position (bottom/top) for mailbox view — merged via #298/#303
- Configurable back button position for single mail view — merged via #299/#307 features in #300
- Configurable "after mail action" (next message / return to mailbox) — merged via #300/#308
- Archive button with `resolveMailboxByRole` helper — merged via #287/#291, #286/#290
- User preferences DB schema (v34v36: `user_preferences` table) — in main
- PR #307 and issue #299 closed.
- Issue #315 closed.
## Tasks (2026-05-26)
- **Renovate Bot (Issue #257)**: Renovate Bot runs daily via Forgejo Actions to keep
@@ -237,7 +237,12 @@ class EmailRepositoryImpl implements EmailRepository {
try {
await client.selectMailboxByPath(emailRow.mailboxPath);
final fetch = await client.uidFetchMessage(emailRow.uid, '(BODY.PEEK[])');
final msg = fetch.messages.first;
final msg = fetch.messages.firstOrNull;
if (msg == null) {
throw StateError(
'IMAP server returned no message for UID ${emailRow.uid}.',
);
}
final textBody = msg.decodeTextPlainPart();
final rawHtml = msg.decodeTextHtmlPart();
final htmlBody =
@@ -2812,7 +2817,12 @@ class EmailRepositoryImpl implements EmailRepository {
emailRow.uid,
'BODY.PEEK[]',
);
final msg = fetch.messages.first;
final msg = fetch.messages.firstOrNull;
if (msg == null) {
throw StateError(
'IMAP server returned no message for UID ${emailRow.uid}.',
);
}
final part = msg.getPart(attachment.fetchPartId) ?? msg;
final bytes = part.decodeContentBinary();
if (bytes == null) {
@@ -2878,7 +2888,13 @@ class EmailRepositoryImpl implements EmailRepository {
emailRow.uid,
'BODY.PEEK[]',
);
return fetch.messages.first.renderMessage();
final msg = fetch.messages.firstOrNull;
if (msg == null) {
throw StateError(
'IMAP server returned no message for UID ${emailRow.uid}.',
);
}
return msg.renderMessage();
} finally {
await client.logout();
}
+11
View File
@@ -163,6 +163,17 @@ class _EmailMessageCardState extends ConsumerState<_EmailMessageCard> {
FutureBuilder<EmailBody>(
future: _bodyFuture,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Padding(
padding: const EdgeInsets.all(16),
child: Text(
'Failed to load email: ${snapshot.error}',
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
);
}
if (!snapshot.hasData) {
return const Center(
child: Padding(
File diff suppressed because it is too large Load Diff
+6
View File
@@ -24,6 +24,12 @@ for attempt in $(seq 1 $MAX_PROBE_ATTEMPTS); do
fi
if [ "$attempt" -eq "$MAX_PROBE_ATTEMPTS" ]; then
echo "Warning: No Dagger server responded on $host:$port after $MAX_PROBE_ATTEMPTS attempts"
if ! docker info >/dev/null 2>&1; then
echo "Error: Remote Dagger engine is unavailable AND local Docker daemon is not running."
echo "Cannot proceed. Ensure either the remote server at $host:$port is accessible"
echo "or that Docker is running locally (check: sudo systemctl start docker)."
exit 1
fi
echo "Remote engine unavailable — CI will use the local Dagger engine."
exit 0
fi
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB