Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa61352b92 | ||
|
|
16db5acb3a | ||
|
|
b013c68988 | ||
|
|
cb2d158689 | ||
|
|
1eb9147ba1 | ||
|
|
4145f7a50b | ||
|
|
c55a27d710 | ||
|
|
15e697ddf3 | ||
|
|
c63895f4da | ||
|
|
aebae1cea5 | ||
|
|
270b13834a | ||
|
|
4090a44524 | ||
|
|
540dbdc0e0 | ||
|
|
31b31c7635 | ||
|
|
1290e3b222 | ||
|
|
248a5bdbcd | ||
|
|
8be84cc396 |
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
name: do-task
|
||||||
|
description: Use when handling a Gitea issue/task end-to-end, including validation, planning, implementation, PR creation, and review-feedback iteration.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Do Task
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Process a single Gitea issue from analysis to merge-ready PR with explicit decision gates. Prefer Gitea MCP tools for all issue/PR actions.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Load issue**
|
||||||
|
- Read issue by ID from Gitea.
|
||||||
|
- Extract: problem, expected behavior, impact, acceptance checks.
|
||||||
|
|
||||||
|
2. **Validate technical correctness**
|
||||||
|
- Verify against current code/docs/tests.
|
||||||
|
- Decide:
|
||||||
|
- **Valid issue** -> continue to planning.
|
||||||
|
- **Not valid / out of scope** -> comment rationale on issue and stop.
|
||||||
|
|
||||||
|
3. **If valid: plan and comment**
|
||||||
|
- Create implementation plan using Superpowers planning flow.
|
||||||
|
- Post summary plan to the issue before coding.
|
||||||
|
|
||||||
|
4. **Implement**
|
||||||
|
- Use Superpowers execution flow (TDD + verification before completion).
|
||||||
|
- Keep changes scoped strictly to issue requirements.
|
||||||
|
|
||||||
|
5. **Create PR**
|
||||||
|
- Open PR from feature branch.
|
||||||
|
- PR description must include:
|
||||||
|
- Summary of changes
|
||||||
|
- Test evidence (exact commands)
|
||||||
|
- `Fixes #<issue-id>` (or `Closes #<issue-id>`)
|
||||||
|
- Add reviewer: `codex`.
|
||||||
|
|
||||||
|
6. **Review feedback loop**
|
||||||
|
- Read all feedback.
|
||||||
|
- For each item:
|
||||||
|
- If technically valid -> implement + re-test + reply.
|
||||||
|
- If not valid -> reply with concise technical reasoning.
|
||||||
|
- Do not blindly accept external feedback without verification.
|
||||||
|
|
||||||
|
## Required Rules
|
||||||
|
- Use Gitea MCP tools first for issue/PR/review operations.
|
||||||
|
- Do not use `tea` or other CLI Gitea clients unless MCP is unavailable.
|
||||||
|
- Never claim completion without fresh verification output.
|
||||||
|
|
||||||
|
## Quick Command Pattern
|
||||||
|
- Invoke as: `/do-task <issue-id>`
|
||||||
|
- Example: `/do-task 12`
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
name: implement-task
|
||||||
|
description: Phase 2 skill. Use when issue scope is locked by Product Owner (Phase 1 done). Plan, implement, create PR, and own it until merged as Developer.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Implement Task
|
||||||
|
|
||||||
|
**Phase:** 2 — Requirement Implementation
|
||||||
|
**Role:** Developer
|
||||||
|
|
||||||
|
## When to invoke
|
||||||
|
|
||||||
|
- User says: "implement issue #X", "/implement-task #X"
|
||||||
|
- Phase 1 is complete (issue has a "Final Requirements" comment from PO).
|
||||||
|
|
||||||
|
## Prerequisite
|
||||||
|
|
||||||
|
- Issue must have a **Final Requirements** comment locked by PO. If not found, stop and tell the user to run `/validate-issue` first.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Load issue**
|
||||||
|
- Read issue: `mcp__gitea__.issue_read` (`method: "get"`).
|
||||||
|
- Read comments, find the **Final Requirements** comment: `mcp__gitea__.issue_read` (`method: "get_comments"`).
|
||||||
|
- If Final Requirements not found → stop, tell user to validate issue first.
|
||||||
|
|
||||||
|
2. **Read codebase**
|
||||||
|
- Use `mcp__gitea__.get_file_contents`, `mcp__gitea__.get_repository_tree` to understand current state.
|
||||||
|
- Identify files and functions that need to change.
|
||||||
|
|
||||||
|
3. **Write plan and post on issue**
|
||||||
|
- List implementation steps with verification criteria.
|
||||||
|
- List files to change.
|
||||||
|
- Flag risks or tradeoffs.
|
||||||
|
- Post plan as comment: `mcp__gitea__.issue_write` (`method: "add_comment"`).
|
||||||
|
- **Wait for "Approved" before coding.** Stop here.
|
||||||
|
|
||||||
|
4. **Implement** (only after plan is approved)
|
||||||
|
- Create feature branch from `main`:
|
||||||
|
```
|
||||||
|
git checkout main && git pull origin main
|
||||||
|
git checkout -b feat/<short-description>
|
||||||
|
```
|
||||||
|
- If currently on another branch, use worktree:
|
||||||
|
```
|
||||||
|
git worktree add .worktrees/<branch-name> main
|
||||||
|
```
|
||||||
|
- Implement following the plan (surgical changes only).
|
||||||
|
- Run validation:
|
||||||
|
```
|
||||||
|
gofmt -l .
|
||||||
|
go vet ./...
|
||||||
|
go test ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Create PR**
|
||||||
|
- Use `mcp__gitea__.pull_request_write` (`method: "create"`).
|
||||||
|
- PR body must include:
|
||||||
|
- Summary of changes
|
||||||
|
- Test evidence (exact commands and output)
|
||||||
|
- `Fixes #<issue-id>`
|
||||||
|
- Add reviewer.
|
||||||
|
|
||||||
|
6. **Own the PR until merged**
|
||||||
|
- Monitor for review feedback: `mcp__gitea__.pull_request_read` (`method: "get_reviews"`, `get_review_comments`).
|
||||||
|
- When feedback arrives:
|
||||||
|
- Checkout the PR branch (or use worktree if on another branch).
|
||||||
|
- Fix code locally, commit, push.
|
||||||
|
- Reply on PR: `mcp__gitea__.pull_request_review_write` (`method: "create"`, `state: "COMMENT"`) explaining what was fixed.
|
||||||
|
- Never edit remote files directly via API.
|
||||||
|
- After reviewer approves (APPROVED), merge the PR if user confirms.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
- Use Gitea MCP tools for all issue/PR operations.
|
||||||
|
- Never edit remote files directly — always work on local branch.
|
||||||
|
- Never claim completion without fresh verification output.
|
||||||
|
- Keep changes strictly scoped to the approved plan.
|
||||||
|
- If the change needs durable documentation, create/update `docs/adr/**`. Do not commit `docs/superpowers/**` planning artifacts.
|
||||||
|
|
||||||
|
## Next skill
|
||||||
|
After PR is created, the user invokes `/review-pr <pr-number>` on the Reviewer agent.
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
---
|
||||||
|
name: review-pr
|
||||||
|
description: Review pull requests in Gitea repositories using Gitea MCP for server-side PR metadata, comments, reviews, diffs, workflow state, and PR comments. Use local CLI tools such as git and test runners only for local repository inspection and validation. Use when the user asks to "Review PR", "review pull request", "check PR", "recheck PR after updates", or "post review comment". Inspect PR metadata and discussion, compare the PR head against its base branch, produce a clear PASS/FAIL verdict, and optionally post the result back to the PR.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Review PR
|
||||||
|
|
||||||
|
## Local Preference
|
||||||
|
|
||||||
|
For repositories on `git.thuanle.me`, prefer the `mcp__gitea__` tools for Gitea-hosted pull request metadata, comments, reviews, server-side diffs, repository contents, branches, commits, workflow runs, and PR comments. Do not use `tea` or other command-line Gitea API clients unless the user explicitly asks for them or the MCP tools are unavailable for the required Gitea operation. Other local CLI tools such as `git`, `go`, `rg`, test runners, and formatters are OK for local repository inspection and validation.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
Follow this sequence:
|
||||||
|
|
||||||
|
1. Identify the PR and current repo context.
|
||||||
|
- Use the current repository when the user is already inside it.
|
||||||
|
- If the user gives only a PR number and the repo context is ambiguous, ask a short clarifying question.
|
||||||
|
- Do not ask follow-up questions when the repository, PR, and requested reviewer action are already clear.
|
||||||
|
- If the user has already given a standing instruction earlier in the same thread such as "if you're confident, post it", carry that instruction forward for later re-reviews until the user changes it.
|
||||||
|
- Read the PR first with `mcp__gitea__.pull_request_read` using `method: "get"`, and read issue comments with `mcp__gitea__.issue_read` using `method: "get_comments"`.
|
||||||
|
- Read reviews with `mcp__gitea__.pull_request_read` using `method: "get_reviews"` when review state matters.
|
||||||
|
- Note the PR status exactly. If it is already merged or closed, say so explicitly before continuing.
|
||||||
|
|
||||||
|
2. Read the review history before judging the latest update.
|
||||||
|
- Find the latest blocking review comments first.
|
||||||
|
- If the author says they addressed feedback in a follow-up commit, focus on the new commits after that discussion.
|
||||||
|
- Still sanity-check the current full PR diff before concluding `PASS`.
|
||||||
|
|
||||||
|
3. Compare against the real base branch.
|
||||||
|
- Use PR metadata from MCP as the source of truth for base/head refs and SHAs.
|
||||||
|
- Prefer the PR base branch from MCP metadata; use `main` only when the base branch is not obvious from PR metadata.
|
||||||
|
|
||||||
|
4. Inspect the code changes.
|
||||||
|
- Use `mcp__gitea__.pull_request_read` with `method: "get_diff"` to read the PR diff.
|
||||||
|
- Use `mcp__gitea__.get_file_contents`, `mcp__gitea__.get_repository_tree`, and commit/branch MCP reads for surrounding context.
|
||||||
|
- If the user asks for a re-review after updates, optionally diff the latest fix commit range as a helper view, but do not skip the full PR sanity check.
|
||||||
|
|
||||||
|
5. Validate behavior where it matters.
|
||||||
|
- Prefer an isolated worktree when validation should run on the PR snapshot instead of the currently checked out branch.
|
||||||
|
- Use a repository-local worktree under `.worktrees/` by default, for example `git worktree add .worktrees/pr-<pr> origin/<head>`. Before creating it, run `git worktree list` and confirm the repo-local `.worktrees/` path.
|
||||||
|
- For `/Users/tm/working/thuanle/crypto/crypto-price-bot`, PR validation worktrees must be under `.worktrees/pr-<pr>` from the repository root. Do not use `/tmp` for this repo; if `.worktrees/` is unavailable, stop and ask the user before using any fallback path.
|
||||||
|
- Run validation commands inside that worktree.
|
||||||
|
- Remove it afterward with `git worktree remove .worktrees/pr-<pr>` when it is no longer needed.
|
||||||
|
- For other repositories, if `git worktree add` fails because the environment blocks writes to `.git/worktrees`, fall back to exporting a temporary snapshot for validation and say that you used the fallback.
|
||||||
|
- Run repo-appropriate checks when feasible.
|
||||||
|
- Prefer targeted checks first, then broader validation such as `go test ./...` and `go vet ./...` in Go repositories.
|
||||||
|
- Say explicitly when validation could not be run.
|
||||||
|
|
||||||
|
6. Write the verdict for the user.
|
||||||
|
- Start with `PASS` or `FAIL`.
|
||||||
|
- For `FAIL`, list blocking findings first, ordered by severity.
|
||||||
|
- Include file and line references whenever possible.
|
||||||
|
- Explain the concrete behavior impact, not style preferences.
|
||||||
|
- For `PASS`, say that no blocking issues were found and then add any non-blocking notes.
|
||||||
|
|
||||||
|
7. Post a review result proactively by default.
|
||||||
|
- When acting as a reviewer, prefer an official PR review over a plain issue comment if the result should affect PR state.
|
||||||
|
- Default stance: if the verdict is clear and you are confident in it, post the review without waiting for another prompt from the user.
|
||||||
|
- If the user has already asked you to post the result, or has given a standing instruction such as "if you're confident, post it", treat that as continuing authority for later re-reviews in the same thread unless the user revokes or narrows it.
|
||||||
|
- Ask the user only when there is a real ambiguity you cannot safely resolve, such as unclear repo/PR context, unclear whether they explicitly want a local-only verdict instead of a posted review, or a materially uncertain finding.
|
||||||
|
- If the user explicitly asks for a local verdict only, do not post.
|
||||||
|
- Use `mcp__gitea__.pull_request_review_write` for review posting.
|
||||||
|
- For normal review results, prefer a single `create` call with the final `state` and `body`. This is the default path for `PASS`, `FAIL`, and neutral review comments.
|
||||||
|
- Use `submit` only when you intentionally need a multi-step pending review flow, such as staging a pending review first and finalizing it later.
|
||||||
|
- Never send the same review narrative through both `create` and `submit`; that is a process bug and can surface as duplicate review content in the UI.
|
||||||
|
- Map the verdict to review state:
|
||||||
|
- `PASS` -> `APPROVED`
|
||||||
|
- `FAIL` -> `REQUEST_CHANGES`
|
||||||
|
- neutral/non-blocking note only -> `COMMENT`
|
||||||
|
- Use `mcp__gitea__.issue_write` with `method: "add_comment"` only for plain discussion comments that should not change PR review status.
|
||||||
|
- Do not add bracketed author/tool tags such as `[codex]`.
|
||||||
|
- Start directly with the review result or author response, depending on your role in the thread.
|
||||||
|
- Keep the review/comment concise and direct.
|
||||||
|
- Mention the validation commands you actually ran when posting review results.
|
||||||
|
|
||||||
|
## Command Patterns
|
||||||
|
|
||||||
|
Use these MCP operations directly:
|
||||||
|
|
||||||
|
- `mcp__gitea__.pull_request_read` with `method: "get"`
|
||||||
|
- `mcp__gitea__.pull_request_read` with `method: "get_diff"`
|
||||||
|
- `mcp__gitea__.pull_request_read` with `method: "get_reviews"`
|
||||||
|
- `mcp__gitea__.pull_request_read` with `method: "get_review_comments"` when a specific review thread is needed
|
||||||
|
- `mcp__gitea__.issue_read` with `method: "get_comments"`
|
||||||
|
- `mcp__gitea__.get_file_contents`
|
||||||
|
- `mcp__gitea__.get_repository_tree`
|
||||||
|
- `mcp__gitea__.get_commit`
|
||||||
|
- `mcp__gitea__.actions_run_read` for workflow status and logs
|
||||||
|
- `mcp__gitea__.pull_request_review_write` with `method: "create"` for the normal one-shot final review path
|
||||||
|
- Set the final `state` on `create`
|
||||||
|
- Put the final review text on `create`
|
||||||
|
- `mcp__gitea__.pull_request_review_write` with `method: "submit"` only when finishing an intentionally pending review
|
||||||
|
- `mcp__gitea__.issue_write` with `method: "add_comment"` when posting a plain PR comment
|
||||||
|
|
||||||
|
Do not use `tea` for Gitea PR review unless the user explicitly requests it or MCP cannot provide the needed data. Local CLI usage such as `git diff`, `git worktree`, and test commands remains acceptable when it materially improves local validation.
|
||||||
|
|
||||||
|
## Comment Template
|
||||||
|
|
||||||
|
Use this structure when posting a review result:
|
||||||
|
|
||||||
|
```text
|
||||||
|
PASS
|
||||||
|
|
||||||
|
No blocking issues found in the latest update.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- <command 1>
|
||||||
|
- <command 2>
|
||||||
|
|
||||||
|
Non-blocking note:
|
||||||
|
- <optional note>
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
FAIL
|
||||||
|
|
||||||
|
1. [High] <blocking issue with path:line and impact>
|
||||||
|
2. [Medium] <blocking issue with path:line and impact>
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- <command 1>
|
||||||
|
- <command 2>
|
||||||
|
```
|
||||||
|
|
||||||
|
When acting as a reviewer and the user asks to approve/reject or otherwise post the result, also submit the matching official review state:
|
||||||
|
- `PASS` -> `APPROVED`
|
||||||
|
- `FAIL` -> `REQUEST_CHANGES`
|
||||||
|
- neutral note only -> `COMMENT`
|
||||||
|
|
||||||
|
When responding as the PR author rather than reviewer, do not use `PASS`/`FAIL`; write a concise author response that explains what feedback was addressed and which validation ran.
|
||||||
|
|
||||||
|
Do not add filler. Keep the review specific, technical, and actionable.
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
---
|
||||||
|
name: review-pr
|
||||||
|
description: Phase 2 skill. Use when a PR is created or updated. Review PR against locked issue requirements and post verdict as Reviewer. Do NOT edit code on the PR.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Review PR
|
||||||
|
|
||||||
|
**Phase:** 2 — Requirement Implementation
|
||||||
|
**Role:** Reviewer
|
||||||
|
|
||||||
|
## When to invoke
|
||||||
|
|
||||||
|
- User says: "review PR #X", "check PR #X", "/review-pr #X"
|
||||||
|
- A PR is created or updated and needs review.
|
||||||
|
|
||||||
|
## Prerequisite
|
||||||
|
|
||||||
|
- PR exists on Gitea.
|
||||||
|
- The referenced issue has a **Final Requirements** comment from PO (Phase 1 done).
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Load PR and issue**
|
||||||
|
- Read PR: `mcp__gitea__.pull_request_read` (`method: "get"`).
|
||||||
|
- Identify the linked issue from PR body (`Fixes #<id>` or `Closes #<id>`).
|
||||||
|
- Read issue comments, find the **Final Requirements** comment: `mcp__gitea__.issue_read` (`method: "get_comments"`).
|
||||||
|
- Read existing reviews: `mcp__gitea__.pull_request_read` (`method: "get_reviews"`).
|
||||||
|
- Read PR discussion comments: `mcp__gitea__.issue_read` (`method: "get_comments"`), using the PR number as the index.
|
||||||
|
- If PR is already merged or closed, say so and stop.
|
||||||
|
|
||||||
|
2. **Read review history** (for re-reviews)
|
||||||
|
- Get review comments: `mcp__gitea__.pull_request_read` (`method: "get_review_comments"`).
|
||||||
|
- Treat reviews, inline review comments, and PR discussion comments as separate context sources.
|
||||||
|
- If author addressed feedback in a follow-up commit, focus on new commits but still sanity-check the full diff.
|
||||||
|
|
||||||
|
3. **Get the diff**
|
||||||
|
- Use `mcp__gitea__.pull_request_read` (`method: "get_diff"`).
|
||||||
|
- Use `mcp__gitea__.get_file_contents` for surrounding context when needed.
|
||||||
|
|
||||||
|
4. **Validate locally** (when needed)
|
||||||
|
- Create worktree:
|
||||||
|
```
|
||||||
|
git worktree add .worktrees/pr-<number> origin/<head-branch>
|
||||||
|
```
|
||||||
|
- Run checks:
|
||||||
|
```
|
||||||
|
gofmt -l .
|
||||||
|
go vet ./...
|
||||||
|
go test ./...
|
||||||
|
```
|
||||||
|
- Remove worktree when done:
|
||||||
|
```
|
||||||
|
git worktree remove .worktrees/pr-<number>
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Evaluate**
|
||||||
|
- Does PR address **all** Final Requirements from the issue?
|
||||||
|
- Does PR contain unrelated changes or refactoring? → FAIL immediately.
|
||||||
|
- Code correctness, style, test coverage.
|
||||||
|
|
||||||
|
6. **Write verdict**
|
||||||
|
- Start with **PASS** or **FAIL**.
|
||||||
|
- Include file:line references for findings.
|
||||||
|
- Explain concrete impact, not style preferences.
|
||||||
|
|
||||||
|
7. **Post review**
|
||||||
|
- Post proactively if verdict is clear.
|
||||||
|
- Use `mcp__gitea__.pull_request_review_write` (`method: "create"`):
|
||||||
|
- `PASS` → `state: "APPROVED"`
|
||||||
|
- `FAIL` → `state: "REQUEST_CHANGES"`
|
||||||
|
- Non-blocking note → `state: "COMMENT"`
|
||||||
|
- For plain discussion (not changing review state): `mcp__gitea__.issue_write` (`method: "add_comment"`).
|
||||||
|
|
||||||
|
## Review Format
|
||||||
|
|
||||||
|
### PASS
|
||||||
|
```
|
||||||
|
PASS
|
||||||
|
|
||||||
|
No blocking issues found.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- gofmt -l .: OK
|
||||||
|
- go vet ./...: OK
|
||||||
|
- go test ./...: OK
|
||||||
|
|
||||||
|
Non-blocking:
|
||||||
|
- <optional note>
|
||||||
|
```
|
||||||
|
|
||||||
|
### FAIL
|
||||||
|
```
|
||||||
|
FAIL
|
||||||
|
|
||||||
|
1. [High] <issue with path:line and impact>
|
||||||
|
2. [Medium] <issue with path:line and impact>
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- go test ./...: FAIL (<details>)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
- Use Gitea MCP tools for all PR/review operations.
|
||||||
|
- Do NOT edit code on the PR.
|
||||||
|
- FAIL if PR contains unrelated changes or scope creep.
|
||||||
|
- Post review proactively if verdict is clear.
|
||||||
|
- Do not post a verdict until PR discussion comments have been checked.
|
||||||
|
- Mention validation commands you actually ran.
|
||||||
|
- No bracketed author/tool tags.
|
||||||
|
- Accept `docs/adr/**` when it records a direct, durable decision tied to the issue scope.
|
||||||
|
- Treat `docs/superpowers/**` and other agent execution artifacts as scope creep. When rejecting docs, explain whether the content should be deleted entirely or rewritten as an ADR.
|
||||||
|
|
||||||
|
## What happens next
|
||||||
|
- **PASS (APPROVED):** Developer merges the PR.
|
||||||
|
- **FAIL (REQUEST_CHANGES):** Developer fixes feedback, then user re-invokes `/review-pr` on this PR.
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
name: validate-issue
|
||||||
|
description: Phase 1 skill. Use when a Gitea issue is created or updated and needs requirement validation. Analyze, clarify, and lock scope as Product Owner. Do NOT implement.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Validate Issue
|
||||||
|
|
||||||
|
**Phase:** 1 — Requirement Refinement
|
||||||
|
**Role:** Product Owner
|
||||||
|
|
||||||
|
## When to invoke
|
||||||
|
|
||||||
|
- User says: "check issue #X", "validate issue #X", "/validate-issue #X"
|
||||||
|
- A new issue is created or an existing issue is updated with new information.
|
||||||
|
|
||||||
|
## Prerequisite
|
||||||
|
|
||||||
|
- Issue exists on Gitea.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Load issue**
|
||||||
|
- Read issue: `mcp__gitea__.issue_read` (`method: "get"`).
|
||||||
|
- Read comments: `mcp__gitea__.issue_read` (`method: "get_comments"`).
|
||||||
|
|
||||||
|
2. **Analyze**
|
||||||
|
- Is the problem statement clear?
|
||||||
|
- Is the scope well-defined or too broad?
|
||||||
|
- Is it technically feasible?
|
||||||
|
- If needed, read relevant code: `mcp__gitea__.get_file_contents`.
|
||||||
|
|
||||||
|
3. **Decide**
|
||||||
|
- **Unclear or incomplete:**
|
||||||
|
- Post clarifying questions: `mcp__gitea__.issue_write` (`method: "add_comment"`).
|
||||||
|
- Stop. Wait for user to respond, then re-run this skill.
|
||||||
|
- **Clear and feasible:**
|
||||||
|
- Post Final Requirements (see format below).
|
||||||
|
- If the change likely needs a durable ADR, add a note: "ADR recommended: this change locks in a behavior/contract that future work must preserve."
|
||||||
|
- This locks the scope and signals Phase 2 can begin.
|
||||||
|
|
||||||
|
## Final Requirements format
|
||||||
|
|
||||||
|
```
|
||||||
|
## Final Requirements
|
||||||
|
|
||||||
|
**Summary:** <one-line summary>
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
- <requirement 1>
|
||||||
|
- <requirement 2>
|
||||||
|
|
||||||
|
**Out of scope:**
|
||||||
|
- <explicitly excluded items>
|
||||||
|
|
||||||
|
**Acceptance criteria:**
|
||||||
|
- <verifiable check 1>
|
||||||
|
- <verifiable check 2>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
- Use Gitea MCP tools for all operations.
|
||||||
|
- Do NOT write code, create branches, or create PRs.
|
||||||
|
- Do NOT proceed to Phase 2 — that is a separate skill.
|
||||||
|
|
||||||
|
## Next skill
|
||||||
|
After Final Requirements are posted, the user invokes `/implement-task <issue-id>` on the Developer agent.
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
# Agent Instructions
|
||||||
|
|
||||||
|
## 1. Coding Principles
|
||||||
|
|
||||||
|
### 1.1 Simplicity First
|
||||||
|
- No out-of-scope features.
|
||||||
|
- No abstractions for single-use code.
|
||||||
|
- If 50 lines work instead of 200, write 50.
|
||||||
|
|
||||||
|
### 1.2 Surgical Changes
|
||||||
|
- Only change lines directly related to the request.
|
||||||
|
- Never refactor adjacent code unless asked.
|
||||||
|
- Remove orphaned imports/variables/functions your changes created.
|
||||||
|
|
||||||
|
### 1.3 Goal-Driven Execution
|
||||||
|
- Define success criteria before coding.
|
||||||
|
- Verify each step before moving to the next.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Workflow Overview
|
||||||
|
|
||||||
|
All work starts from a Gitea issue. Two phases, each with a dedicated role:
|
||||||
|
|
||||||
|
1. **Phase 1 — Requirement Refinement** (Product Owner): Clarify and lock scope.
|
||||||
|
2. **Phase 2 — Requirement Implementation** (Developer & Reviewer): Plan, code, review, merge.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Phase 1: Requirement Refinement
|
||||||
|
|
||||||
|
**Role:** Product Owner (PO)
|
||||||
|
**Goal:** Turn a raw issue into a clear, scoped, feasible requirement.
|
||||||
|
**Constraint:** PO must NOT write code or create PRs.
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
1. Read and analyze the issue.
|
||||||
|
2. Is the requirement clear and well-scoped?
|
||||||
|
- **No:** Comment clarifying questions on the issue. Stop and wait for user response.
|
||||||
|
- **Yes:** Comment a summary and lock the scope (Final Requirements).
|
||||||
|
3. Locked issue moves to Phase 2.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Phase 2: Requirement Implementation
|
||||||
|
|
||||||
|
**Roles:** Developer (Dev) and Reviewer (Rev)
|
||||||
|
**Goal:** Implement the locked requirement from Phase 1.
|
||||||
|
|
||||||
|
### 4.1 Developer: Plan & Implement
|
||||||
|
|
||||||
|
1. Read the locked issue and any clarification comments.
|
||||||
|
2. Read relevant code to understand current state.
|
||||||
|
3. Write an implementation plan — list steps, files to change, risks.
|
||||||
|
4. **Comment the plan on the issue. Wait for "Approved" before coding.**
|
||||||
|
5. Once approved:
|
||||||
|
- Create a feature branch from `main`.
|
||||||
|
- Implement following the plan (surgical changes only).
|
||||||
|
- Run validation: `go test ./...`, `go vet ./...`, `gofmt -l .`.
|
||||||
|
- Create PR with `Fixes #<issue>` in the body so Gitea auto-closes the issue on merge.
|
||||||
|
- After merge, verify the issue is closed. If not (e.g. missing keyword), close it manually with a summary comment.
|
||||||
|
6. **Own the PR until merged:**
|
||||||
|
- Monitor for review feedback.
|
||||||
|
- Fix feedback on the correct branch (checkout or worktree, never edit remote via API).
|
||||||
|
- Always reply on PR after pushing a fix.
|
||||||
|
|
||||||
|
### 4.2 Reviewer: Review PR
|
||||||
|
|
||||||
|
**Constraint:** Reviewer must NOT edit code on the PR.
|
||||||
|
|
||||||
|
1. Re-read the original issue — verify PR addresses the locked requirement.
|
||||||
|
2. Read PR diff, review history, and PR discussion comments.
|
||||||
|
3. Validate in worktree if needed (`.worktrees/pr-<number>`, run `go test`, `go vet`, `gofmt`).
|
||||||
|
4. Check for scope creep — FAIL if PR contains unrelated changes or refactoring.
|
||||||
|
5. Write verdict — start with **PASS** or **FAIL**.
|
||||||
|
6. Post review proactively if verdict is clear:
|
||||||
|
- `PASS` → `APPROVED`
|
||||||
|
- `FAIL` → `REQUEST_CHANGES`
|
||||||
|
- Non-blocking note → `COMMENT`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Gitea Tools
|
||||||
|
|
||||||
|
- If git remote contains `git.thuanle.me`, ALWAYS use Gitea MCP tools.
|
||||||
|
- Scope: read PR/issue, list comments, post replies, create/edit PRs, reviews.
|
||||||
|
- Use `tea` CLI only as fallback when MCP is unavailable.
|
||||||
|
- Do not use `gh` for Gitea repositories.
|
||||||
|
- Local CLI tools (`git`, `go`, `rg`) are fine for local validation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Review Format
|
||||||
|
|
||||||
|
```
|
||||||
|
PASS
|
||||||
|
|
||||||
|
No blocking issues found.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- go test ./...: OK
|
||||||
|
- go vet ./...: OK
|
||||||
|
|
||||||
|
Non-blocking:
|
||||||
|
- <optional note>
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
FAIL
|
||||||
|
|
||||||
|
1. [High] <issue with path:line and impact>
|
||||||
|
2. [Medium] <issue with path:line and impact>
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- go test ./...: OK
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Documentation Rules
|
||||||
|
|
||||||
|
- Durable architecture or behavior decisions belong in `docs/adr/**`.
|
||||||
|
- `docs/superpowers/**` is for agent execution artifacts and must not be merged as part of product implementation PRs.
|
||||||
|
- If a PR needs a lasting technical rationale, author an ADR in `docs/adr/` instead.
|
||||||
|
- Reviewers should treat temporary plan/spec docs as scope creep, but accept ADRs that directly document the accepted behavior of the change.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Comment Style
|
||||||
|
|
||||||
|
- Use readable multiline markdown.
|
||||||
|
- Use short sections (Summary, Changes, Test Plan).
|
||||||
|
- Concise bullet points and explicit line breaks.
|
||||||
|
- No bracketed author/tool tags.
|
||||||
@@ -1,70 +1,5 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
|
See [AGENT.md](AGENT.md) for full project instructions and workflow.
|
||||||
|
|
||||||
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
|
Agent skills are stored in `.codex/skills`
|
||||||
|
|
||||||
## 1. Think Before Coding
|
|
||||||
|
|
||||||
**Don't assume. Don't hide confusion. Surface tradeoffs.**
|
|
||||||
|
|
||||||
Before implementing:
|
|
||||||
|
|
||||||
- State your assumptions explicitly. If uncertain, ask.
|
|
||||||
- If multiple interpretations exist, present them - don't pick silently.
|
|
||||||
- If a simpler approach exists, say so. Push back when warranted.
|
|
||||||
- If something is unclear, stop. Name what's confusing. Ask.
|
|
||||||
|
|
||||||
## 2. Simplicity First
|
|
||||||
|
|
||||||
**Minimum code that solves the problem. Nothing speculative.**
|
|
||||||
|
|
||||||
- No features beyond what was asked.
|
|
||||||
- No abstractions for single-use code.
|
|
||||||
- No "flexibility" or "configurability" that wasn't requested.
|
|
||||||
- No error handling for impossible scenarios.
|
|
||||||
- If you write 200 lines and it could be 50, rewrite it.
|
|
||||||
|
|
||||||
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
|
|
||||||
|
|
||||||
## 3. Surgical Changes
|
|
||||||
|
|
||||||
**Touch only what you must. Clean up only your own mess.**
|
|
||||||
|
|
||||||
When editing existing code:
|
|
||||||
|
|
||||||
- Don't "improve" adjacent code, comments, or formatting.
|
|
||||||
- Don't refactor things that aren't broken.
|
|
||||||
- Match existing style, even if you'd do it differently.
|
|
||||||
- If you notice unrelated dead code, mention it - don't delete it.
|
|
||||||
|
|
||||||
When your changes create orphans:
|
|
||||||
|
|
||||||
- Remove imports/variables/functions that YOUR changes made unused.
|
|
||||||
- Don't remove pre-existing dead code unless asked.
|
|
||||||
|
|
||||||
The test: Every changed line should trace directly to the user's request.
|
|
||||||
|
|
||||||
## 4. Goal-Driven Execution
|
|
||||||
|
|
||||||
**Define success criteria. Loop until verified.**
|
|
||||||
|
|
||||||
Transform tasks into verifiable goals:
|
|
||||||
|
|
||||||
- "Add validation" → "Write tests for invalid inputs, then make them pass"
|
|
||||||
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
|
|
||||||
- "Refactor X" → "Ensure tests pass before and after"
|
|
||||||
|
|
||||||
For multi-step tasks, state a brief plan:
|
|
||||||
|
|
||||||
```
|
|
||||||
1. [Step] → verify: [check]
|
|
||||||
2. [Step] → verify: [check]
|
|
||||||
3. [Step] → verify: [check]
|
|
||||||
```
|
|
||||||
|
|
||||||
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
|
|
||||||
|
|||||||
+17
-1
@@ -3,10 +3,12 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"thuanle.me/claw-email-bridge/internal/ai_client"
|
"thuanle.me/claw-email-bridge/internal/ai_client"
|
||||||
"thuanle.me/claw-email-bridge/internal/api"
|
"thuanle.me/claw-email-bridge/internal/api"
|
||||||
@@ -62,16 +64,30 @@ func main() {
|
|||||||
// Start HTTP server with SMTP egress on callback.
|
// Start HTTP server with SMTP egress on callback.
|
||||||
smtpSender := mail.NewSMTPSender(cfg, db)
|
smtpSender := mail.NewSMTPSender(cfg, db)
|
||||||
router := api.NewRouter(db, cfg.BridgeCallbackToken, smtpSender.SendReplyFunc())
|
router := api.NewRouter(db, cfg.BridgeCallbackToken, smtpSender.SendReplyFunc())
|
||||||
|
srv := &http.Server{Addr: cfg.ListenAddr, Handler: router}
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
slog.Info("starting HTTP server", "addr", cfg.ListenAddr)
|
slog.Info("starting HTTP server", "addr", cfg.ListenAddr)
|
||||||
if err := router.Run(cfg.ListenAddr); err != nil {
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
slog.Error("server exited with error", "error", err)
|
slog.Error("server exited with error", "error", err)
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Graceful shutdown: wait for context cancel then shutdown HTTP server.
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
<-ctx.Done()
|
||||||
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer shutdownCancel()
|
||||||
|
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||||
|
slog.Error("server shutdown error", "error", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
slog.Info("bridge stopped")
|
slog.Info("bridge stopped")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
type: ADR
|
||||||
|
id: "0001"
|
||||||
|
title: "IMAP thread resolution prefers In-Reply-To and falls back to References"
|
||||||
|
status: active
|
||||||
|
date: 2026-04-28
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The IMAP ingress flow stores a `thread_id` before dispatching work to OpenClaw. Previously the bridge only used `In-Reply-To` to find a parent task. Some valid email clients preserve thread ancestry through `References` even when `In-Reply-To` is absent. In those cases, the bridge created a new `thread_id`, which broke conversation continuity and downstream session reuse.
|
||||||
|
|
||||||
|
This decision needs to be durable because future refactors of mail parsing, dispatch, or storage must preserve the same thread-resolution behavior.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
The bridge resolves parent thread linkage by trying `In-Reply-To` first. If no parent task is found, it falls back to `References`, scanning message IDs from last to first and using the first match found in the database. If neither header yields a known parent, the message's own `message_id` becomes the new `thread_id`.
|
||||||
|
|
||||||
|
## Options considered
|
||||||
|
|
||||||
|
- **Use `In-Reply-To` first, then `References` last-to-first** (chosen): preserves existing behavior, matches common email ancestry ordering, and favors the most recent known parent.
|
||||||
|
- **Use only `In-Reply-To`**: simpler, but breaks valid threads when clients omit `In-Reply-To`.
|
||||||
|
- **Search `References` from first to last**: may attach to an older ancestor even when a more recent known parent exists.
|
||||||
|
- **Normalize all thread linkage into a more complex conversation graph**: more flexible, but out of scope for the current bridge behavior.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
Thread continuity is improved for replies that rely on `References`.
|
||||||
|
Future contributors have an explicit behavioral contract for threading semantics.
|
||||||
|
Mail parsing changes must preserve the header precedence and reverse scan behavior unless a new ADR supersedes this one.
|
||||||
|
The current approach still depends on existing message IDs being present in the database; if upstream behavior changes significantly, this decision may need reevaluation.
|
||||||
|
|
||||||
|
## Advice
|
||||||
|
|
||||||
|
If a future change wants different precedence rules or deeper conversation reconstruction, create a new ADR rather than silently changing this behavior.
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# Architecture Decision Records
|
||||||
|
|
||||||
|
This directory contains Architecture Decision Records (ADRs) for durable technical decisions in this repository.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
ADRs capture cross-cutting or behavior-defining decisions that future contributors need to preserve.
|
||||||
|
|
||||||
|
Use an ADR when a change introduces or locks in a decision such as:
|
||||||
|
|
||||||
|
- runtime behavior that future work must preserve
|
||||||
|
- repository-wide technical policy
|
||||||
|
- architecture or integration contracts
|
||||||
|
- storage or threading semantics
|
||||||
|
- quality gate or workflow policy
|
||||||
|
|
||||||
|
Do not use ADRs for:
|
||||||
|
|
||||||
|
- temporary implementation plans
|
||||||
|
- agent execution checklists
|
||||||
|
- one-off debugging notes
|
||||||
|
- PR-local task breakdowns
|
||||||
|
|
||||||
|
## Format
|
||||||
|
|
||||||
|
Each ADR is a markdown file with YAML frontmatter.
|
||||||
|
|
||||||
|
```md
|
||||||
|
---
|
||||||
|
type: ADR
|
||||||
|
id: "NNNN"
|
||||||
|
title: "Short decision title"
|
||||||
|
status: active
|
||||||
|
date: YYYY-MM-DD
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Why does this decision need to exist now?
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**State the decision clearly in one or two sentences.**
|
||||||
|
|
||||||
|
## Options considered
|
||||||
|
|
||||||
|
- **Option A** (chosen): description with pros and cons
|
||||||
|
- **Option B**: description with pros and cons
|
||||||
|
- **Option C**: description with pros and cons
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
What becomes easier?
|
||||||
|
What becomes harder?
|
||||||
|
What risks were accepted?
|
||||||
|
What would trigger re-evaluation?
|
||||||
|
|
||||||
|
## Advice
|
||||||
|
|
||||||
|
Optional. Capture important external input or review notes.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- One decision per file.
|
||||||
|
- Name files `NNNN-short-kebab-title.md`.
|
||||||
|
- Once active, do not rewrite an ADR's meaning later. Supersede it with a new ADR instead.
|
||||||
|
- Keep the index below aligned with the current files in this directory.
|
||||||
|
- Do not keep placeholder or mock entries in the index.
|
||||||
|
|
||||||
|
## Index
|
||||||
|
|
||||||
|
| ID | Title | Status |
|
||||||
|
|----|-------|--------|
|
||||||
|
| 0001 | IMAP thread resolution prefers In-Reply-To and falls back to References | active |
|
||||||
+40
-7
@@ -283,6 +283,39 @@ func (w *IMAPWatcher) fetchUnseen(c *imapclient.Client) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolveThreadID determines the thread_id for a message.
|
||||||
|
// It tries In-Reply-To first, then falls back to References (last-to-first).
|
||||||
|
// Returns the message's own ID if no parent is found.
|
||||||
|
func resolveThreadID(messageID, inReplyTo, references string, lookup func(string) (*database.Task, error)) string {
|
||||||
|
if inReplyTo != "" {
|
||||||
|
if parent, err := lookup(inReplyTo); err == nil && parent != nil {
|
||||||
|
return parent.ThreadID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, refID := range parseReferences(references) {
|
||||||
|
if parent, err := lookup(refID); err == nil && parent != nil {
|
||||||
|
return parent.ThreadID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return messageID
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseReferences splits a References header value into individual message IDs,
|
||||||
|
// returned in reverse order (most recent first).
|
||||||
|
func parseReferences(header string) []string {
|
||||||
|
if header == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
raw := strings.Fields(header)
|
||||||
|
// Reverse: last (most recent) first.
|
||||||
|
for i, j := 0, len(raw)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
raw[i], raw[j] = raw[j], raw[i]
|
||||||
|
}
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
// processMessage handles a single fetched email message.
|
// processMessage handles a single fetched email message.
|
||||||
func (w *IMAPWatcher) processMessage(c *imapclient.Client, buf *imapclient.FetchMessageBuffer, bodySection *imap.FetchItemBodySection) {
|
func (w *IMAPWatcher) processMessage(c *imapclient.Client, buf *imapclient.FetchMessageBuffer, bodySection *imap.FetchItemBodySection) {
|
||||||
env := buf.Envelope
|
env := buf.Envelope
|
||||||
@@ -333,6 +366,7 @@ func (w *IMAPWatcher) processMessage(c *imapclient.Client, buf *imapclient.Fetch
|
|||||||
// 4. Threading: determine thread_id.
|
// 4. Threading: determine thread_id.
|
||||||
threadID := messageID
|
threadID := messageID
|
||||||
inReplyTo := ""
|
inReplyTo := ""
|
||||||
|
references := ""
|
||||||
|
|
||||||
// Parse body to get In-Reply-To / References headers and plain text.
|
// Parse body to get In-Reply-To / References headers and plain text.
|
||||||
bodyPlain := ""
|
bodyPlain := ""
|
||||||
@@ -340,10 +374,12 @@ func (w *IMAPWatcher) processMessage(c *imapclient.Client, buf *imapclient.Fetch
|
|||||||
if rawBody != nil {
|
if rawBody != nil {
|
||||||
mr, err := gomessage.CreateReader(io.NopCloser(strings.NewReader(string(rawBody))))
|
mr, err := gomessage.CreateReader(io.NopCloser(strings.NewReader(string(rawBody))))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Extract In-Reply-To header.
|
|
||||||
if irt, err := mr.Header.Text("In-Reply-To"); err == nil && irt != "" {
|
if irt, err := mr.Header.Text("In-Reply-To"); err == nil && irt != "" {
|
||||||
inReplyTo = strings.TrimSpace(irt)
|
inReplyTo = strings.TrimSpace(irt)
|
||||||
}
|
}
|
||||||
|
if refs, err := mr.Header.Text("References"); err == nil && refs != "" {
|
||||||
|
references = refs
|
||||||
|
}
|
||||||
|
|
||||||
// Read body parts for plain text.
|
// Read body parts for plain text.
|
||||||
for {
|
for {
|
||||||
@@ -361,12 +397,9 @@ func (w *IMAPWatcher) processMessage(c *imapclient.Client, buf *imapclient.Fetch
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if inReplyTo != "" {
|
threadID = resolveThreadID(messageID, inReplyTo, references, func(mid string) (*database.Task, error) {
|
||||||
parent, err := database.FindByMessageID(w.db, inReplyTo)
|
return database.FindByMessageID(w.db, mid)
|
||||||
if err == nil && parent != nil {
|
})
|
||||||
threadID = parent.ThreadID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Save task with status RECEIVED.
|
// 5. Save task with status RECEIVED.
|
||||||
taskUUID := uuid.New().String()
|
taskUUID := uuid.New().String()
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"thuanle.me/claw-email-bridge/internal/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResolveThreadID_NoHeaders_ReturnsMessageID(t *testing.T) {
|
||||||
|
got := resolveThreadID("msg-1", "", "", nilLookup)
|
||||||
|
if got != "msg-1" {
|
||||||
|
t.Fatalf("expected msg-1, got %s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveThreadID_InReplyToMatches_ReturnsParentThreadID(t *testing.T) {
|
||||||
|
lookup := func(messageID string) (*database.Task, error) {
|
||||||
|
if messageID == "<parent>" {
|
||||||
|
return &database.Task{ThreadID: "thread-abc"}, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
got := resolveThreadID("msg-2", "<parent>", "", lookup)
|
||||||
|
if got != "thread-abc" {
|
||||||
|
t.Fatalf("expected thread-abc, got %s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveThreadID_ReferencesOnly_LastIDMatches(t *testing.T) {
|
||||||
|
lookup := func(messageID string) (*database.Task, error) {
|
||||||
|
if messageID == "<C>" {
|
||||||
|
return &database.Task{ThreadID: "thread-c"}, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
got := resolveThreadID("msg-3", "", "<A> <B> <C>", lookup)
|
||||||
|
if got != "thread-c" {
|
||||||
|
t.Fatalf("expected thread-c, got %s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveThreadID_ReferencesOnly_MiddleIDMatches(t *testing.T) {
|
||||||
|
lookup := func(messageID string) (*database.Task, error) {
|
||||||
|
if messageID == "<B>" {
|
||||||
|
return &database.Task{ThreadID: "thread-b"}, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
got := resolveThreadID("msg-4", "", "<A> <B> <C>", lookup)
|
||||||
|
if got != "thread-b" {
|
||||||
|
t.Fatalf("expected thread-b, got %s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveThreadID_ReferencesOnly_NoMatch_ReturnsMessageID(t *testing.T) {
|
||||||
|
got := resolveThreadID("msg-5", "", "<X> <Y>", nilLookup)
|
||||||
|
if got != "msg-5" {
|
||||||
|
t.Fatalf("expected msg-5, got %s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveThreadID_InReplyToMiss_ReferencesMatches(t *testing.T) {
|
||||||
|
lookup := func(messageID string) (*database.Task, error) {
|
||||||
|
if messageID == "<ref>" {
|
||||||
|
return &database.Task{ThreadID: "thread-ref"}, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
got := resolveThreadID("msg-6", "<unknown>", "<ref>", lookup)
|
||||||
|
if got != "thread-ref" {
|
||||||
|
t.Fatalf("expected thread-ref, got %s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveThreadID_LookupError_ReturnsMessageID(t *testing.T) {
|
||||||
|
lookup := func(messageID string) (*database.Task, error) {
|
||||||
|
return nil, errors.New("db error")
|
||||||
|
}
|
||||||
|
got := resolveThreadID("msg-7", "<parent>", "", lookup)
|
||||||
|
if got != "msg-7" {
|
||||||
|
t.Fatalf("expected msg-7, got %s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var nilLookup = func(string) (*database.Task, error) { return nil, nil }
|
||||||
Reference in New Issue
Block a user