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
7 changed files with 57 additions and 2204 deletions
-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
+27 -32
View File
@@ -8,46 +8,41 @@ CLI tool `fgj` is available to query issues/PRs/actions.
## Issue Label Workflow ## 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 | Label | Trigger | Outcome |
- **State/Planned** — Plan has been posted as a comment; awaiting human review |---|---|---|
- **State/Ready** — Issue is approved and ready for implementation | `loop/plan` | Planning agent reads the issue and writes an implementation plan as a comment | Issue moves to `loop/plan-done` |
- **State/InProgress** — Set while an agent (or human) is actively working | `loop/code` | Coding agent implements the change, creates a branch + PR | Issue moves to `loop/code-done` |
- **State/Question** — Agent hit a blocker or needs clarification
Full lifecycle: **State machine:**
``` ```
State/ToPlan → State/Planned (automated: agent_loop.py runs a planning agent) loop/plan loop/plan-in-progress → loop/plan-done
State/Planned → State/Ready (manual: human reviews the plan and approves) ↘ NeedSupervisor (on failure)
State/Ready → State/InProgress (automated: agent_loop.py before starting implementation)
State/InProgress → closed (automated: after PR is merged and CI passes) loop/code → loop/code-in-progress loop/code-done
any state → State/Question (automated or manual: when blocked) 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}'
``` ```
1. Create issue
Rules: 2. Add label loop/plan → agent writes plan as comment
3. Review plan, request changes or approve
- Never start implementation on an issue without `State/Ready` 4. Add label loop/code → agent implements + opens PR
- Planning agents only post a plan comment — they do NOT write code or open PRs 5. Review PR, merge
- After `State/Planned`, a human must review the plan and manually add `State/Ready` 6. Close issue
- 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>
```
## Code conventions ## Code conventions
-2
View File
@@ -188,5 +188,3 @@ Using SSH to `localhost` is preferred over complex X11/Wayland permission hacks.
## Daily Workflow ## Daily Workflow
Refer to the [README.md](./README.md#daily-workflow) for common development tasks and commands. Refer to the [README.md](./README.md#daily-workflow) for common development tasks and commands.
<!-- agentloop code test passed -->
@@ -237,7 +237,12 @@ class EmailRepositoryImpl implements EmailRepository {
try { try {
await client.selectMailboxByPath(emailRow.mailboxPath); await client.selectMailboxByPath(emailRow.mailboxPath);
final fetch = await client.uidFetchMessage(emailRow.uid, '(BODY.PEEK[])'); 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 textBody = msg.decodeTextPlainPart();
final rawHtml = msg.decodeTextHtmlPart(); final rawHtml = msg.decodeTextHtmlPart();
final htmlBody = final htmlBody =
@@ -2812,7 +2817,12 @@ class EmailRepositoryImpl implements EmailRepository {
emailRow.uid, emailRow.uid,
'BODY.PEEK[]', '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 part = msg.getPart(attachment.fetchPartId) ?? msg;
final bytes = part.decodeContentBinary(); final bytes = part.decodeContentBinary();
if (bytes == null) { if (bytes == null) {
@@ -2878,7 +2888,13 @@ class EmailRepositoryImpl implements EmailRepository {
emailRow.uid, emailRow.uid,
'BODY.PEEK[]', '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 { } finally {
await client.logout(); await client.logout();
} }
+11
View File
@@ -163,6 +163,17 @@ class _EmailMessageCardState extends ConsumerState<_EmailMessageCard> {
FutureBuilder<EmailBody>( FutureBuilder<EmailBody>(
future: _bodyFuture, future: _bodyFuture,
builder: (context, snapshot) { 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) { if (!snapshot.hasData) {
return const Center( return const Center(
child: Padding( child: Padding(
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff