私は、私のTodoistを読み取り、タスクをコード化して、完了としてマークするGitHub Actionを作った——5時間ごとに
はい、5時間です。いいえ、ランダムではありません。読み進めてください。
自分ひとりで開発しているときの問題点
Todoistのボードが完璧に整理されていて、コーヒーも用意できているのに、コードエディタを開く気力がゼロになる——そんな感覚、分かりますよね?
同じです。
私はフロントエンドエンジニアとして、自分ひとりでプロダクトを作っています。日々の仕事、コンテンツ制作、そしてジムに行くフリをしながらも集中できる時間を見つけてバックログを少しずつ削るのは……なかなか大変です。
そこで、合理的な開発者なら誰でもやることをしました。つまり、問題そのものを自動化で消したのです。
アイデア:エージェント用のキュー
Agent_QueueというTodoistプロジェクトを作りました。小さなコーディングタスク——リファクタリング、新しいコンポーネント、バグ修正——が発生するたびに、AIが理解できるだけの十分に詳しい説明を付けてタスクとして追加します。
次にGitHub Actionが起動し、優先度が最も高いタスクを取り出してClaude Codeに渡します。Claude Codeにやるべきことをやらせて、その結果を私のdevブランチにコミットし、Todoist上でそのタスクを完了としてマークします。
私は朝起きるとコードが書かれています。うまくいくときもあれば、調整が必要なときもあります。でも、自分で作らなくてよい「スタート地点」ができたのが大きいです。
なぜ5時間ごと?
では物語です。
Claude Codeは内部でAPIトークンを使います。これらのトークンには利用上限があり、タスクに対してClaude Codeを自律的に動かしていると……かなり積極的になり得ます。「なぜなら、全コードベースを書き直してしまおう」みたいなノリです。
それを5時間ごとに実行すると、トークン利用の時間を確保して、実行の間に呼吸してリセットできます。さらに、何かがうまくいかない場合(Claudeが暴走する、混沌をコミットする、APIが一時的に不調になるなど)でも、対処すべき被害は1つのタスク分だけで済みます——徹夜で発生した47件もの自律コミットの山を相手にすることにはなりません。
たとえばこう考えてください:1タスク、1セッション、1回の息。5時間の間隔は、Claudeが「取っているはずの休憩時間」を知らないまま用意してくれるコーヒーブレイクです。
また、Anthropicのレート制限は本物です。守りましょう。財布も喜びます。
完全なワークフロー
以下が、仕組みの全手順です:
1. トリガー
on:
schedule:
- cron: '0 */5 * * *' # every 5 hours
workflow_dispatch: # or manually, when you're impatient
workflow_dispatchは緊急時の上書きです。Todoistで何か直したときに、「とにかく今すぐ実行したい」ことがあります。私はこれを常に使っています。
2. プロジェクト + 最上位タスクを見つける
アクションがTodoist APIにアクセスし、名前でAgent_Queueプロジェクトを見つけ、すべてのタスクを取得して優先度でソートします。TodoistのAPIではP1 = priority 4です(はい、逆です。聞かないでください。そういう仕様です)。
- name: Fetch top priority task from Todoist
id: todoist
env:
TODOIST_API_KEY: ${{ secrets.TODOIST_API_KEY }}
TODOIST_PROJECT_NAME: 'Agent_Queue'
run: |
PROJECTS=$(curl -s \
-H "Authorization: Bearer $TODOIST_API_KEY" \
https://api.todoist.com/api/v1/projects)
PROJECT_ID=$(echo "$PROJECTS" | python3 -c "
import json, sys, os
data = json.load(sys.stdin)
name = os.environ['TODOIST_PROJECT_NAME']
match = next((p for p in data if p['name'] == name), None)
print(match['id'] if match else '')
")
JSONのパースにはインラインPythonを使っています。余計な依存関係はなく、セットアップも不要です。ちょっと呪術っぽいですが、ちゃんと美しく動きます。
3. Claude Codeを実行
ここが魔法のステップです。Claude Codeはターミナルで動き、コードベース全体を読み取り、タスクのプロンプトを自律的に実行します。
- name: Install Claude Code
run: npm install -g @anthropic-ai/claude-code
- name: Run Claude Code on task
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
TASK_PROMPT: ${{ steps.todoist.outputs.task_prompt }}
run: |
echo "$TASK_PROMPT" | claude -p \
--dangerously-skip-permissions \
--max-turns 10 \
--output-format stream-json \
--verbose 2>&1 | tee claude-output.log
挙げておくべきフラグがいくつかあります:
-
--dangerously-skip-permissions— Claude Codeは通常、何かを実行する前に確認を求めます。CIには確認してくれる人がいないので、スキップします。これは敬意を持って使ってください。 -
--max-turns 10— Claudeが行う往復の回数にハードな上限を設定します。無限ループを防ぎます。 -
--output-format stream-json— 何が起きたかを実際にデバッグできるよう、すべてをJSONストリームとしてログ出力します。
4. devにコミット
- name: Commit any changes Claude made
run: |
git config user.name "Claude Agent"
git config user.email "claude-agent@users.noreply.github.com"
git add -A
if git diff --staged --quiet; then
echo "No file changes to commit."
else
git commit -m "chore: agent task - ${{ steps.todoist.outputs.task_content }}"
git push
fi
Claudeがファイルの変更を何もしていない場合(タスクがすでに完了していた、あるいは読み取り専用のタスクだった等)、ワークフローは問題なく終了します。空コミットはありません。
5. Todoistでタスクを完了としてマーク
返却形式: {"translated": "翻訳されたHTML"}- name: Todoist でタスクを完了としてマークする
if: steps.todoist.outputs.task_found == 'true' && success()
env:
TODOIST_API_KEY: ${{ secrets.TODOIST_API_KEY }}
TASK_ID: ${{ steps.todoist.outputs.task_id }}
run: |
curl -s -X POST \
-H "Authorization: Bearer $TODOIST_API_KEY" \
-H "Content-Type: application/json" \
"https://api.todoist.com/api/v1/tasks/$TASK_ID/close"
success() の条件は重要です — それ以前のすべてが成功した場合にのみ、タスクが完了としてマークされます。Claude がクラッシュした、またはプッシュに失敗した場合、タスクは開いたままで、次のサイクルで再びピックアップされます。
GitHub Secrets You Need
リポジトリへ移動 → Settings → Secrets and variables → Actions で、以下を追加します:
| Secret | 入手先 |
|---|---|
TODOIST_API_KEY |
Todoist → Settings → Integrations → Developer |
ANTHROPIC_API_KEY |
console.anthropic.com |
他のタスクツールでも動きますか?
はい — そしてここが私の大好きなところです。GitHub Actions の構造は完全に汎用的です。必要なのは、次のことができる REST API を持つタスクツールだけです:
- タスクを一覧表示(優先度や並び順などを含む)
- ID によってタスクをクローズ/完了する
つまり、Todoist を次のように差し替えられます:
- Linear — チーム向けで、API もとても良いです
- Jira — エンタープライズ向けで、より冗長ですが動きます
- Asana — 同じコンセプトで、エンドポイントが異なります
- Notion — データベースをタスクリストとして扱えるので、完全に可能
- GitHub Issues — メタ要素が極まっていますが、動きます
変更するのは、fetch と close のステップで使う curl コマンドだけです。それ以外 — Claude Code のランナー、git のコミット、cron のスケジュール — はまったく同じままです。
しばらく運用してみてのヒント
詳細なタスク説明を書く。 タスクの内容が Claude Code のプロンプトになります。「バグを直して」ではどうにもなりません。「ユーザーが空のメールを送信するとログインページで TypeError が発生する — AuthForm.tsx の null チェックを修正して」が、ちゃんと動く修正につながります。
タイトルだけでなく、タスク説明欄を使う。 私のワークフローでは、存在する場合は説明文を完全なプロンプトとして使い、なければタイトルを使います。ファイル名、期待される挙動、これまでに試したことなど、Claude に文脈を渡してください。
小さなタスクから始める。 初日から「認証モジュール全体をリファクタして」と投げないでください。まずは「LoginForm.tsx の送信ボタンに不足している aria-label を追加して」から始めましょう。エージェントに王国の鍵を渡す前に、信頼を積み上げます。
マージ前に確認する。 これは main ではなく dev にコミットします。マージする前に Claude が出したものを必ずレビューしてください。だいたいは良い、時にはとても良い、たまに理解不能なことがあります。
その雰囲気
私はこれを作りました。最高のツールは、あなたが寝ている間でも動いてくれるものだと信じているからです。「ハスル文化」みたいな話ではなくて、「コンピュータが反復作業をやってくれるので、あなたの頭がやらなくて済む」という意味です。
私の Todoist ボードは、今ではチームのスタンドアップにもなっています。私はタスクを書く。エージェントがそれを拾って実行し、朝座ったときには、私がレビューするためのコミットが待っています。ちょっと変で、でも楽しいワークフローで、私はこれに全力で賛同しています。
完全な YAML
完成したワークフローのファイルは以下です。これを .github/workflows/todoist-sync.yml にドロップすれば完了です。
name: Agent Queue Runner
on:
schedule:
- cron: '0 */5 * * *'
workflow_dispatch:
jobs:
run-agent-task:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
ref: dev
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Fetch top priority task from Todoist
id: todoist
env:
TODOIST_API_KEY: ${{ secrets.TODOIST_API_KEY }}
TODOIST_PROJECT_NAME: 'Agent_Queue'
run: |
PROJECTS=$(curl -s \
-H "Authorization: Bearer $TODOIST_API_KEY" \
-H "Content-Type: application/json" \
https://api.todoist.com/api/v1/projects)
if [ -z "$PROJECTS" ]; then
echo "Todoist API からの空のレスポンスです。"
exit 1
fi
返却形式: {"translated": "翻訳されたHTML"}PROJECT_ID=$(echo "$PROJECTS" | python3 -c "
import json, sys, os
data = json.load(sys.stdin)
projects = data.get('results', data) if isinstance(data, dict) else data
name = os.environ['TODOIST_PROJECT_NAME']
match = next((p for p in projects if p['name'] == name), None)
print(match['id'] if match else '')
")
if [ -z "$PROJECT_ID" ]; then
echo "Project not found."
echo "task_found=false" >> $GITHUB_OUTPUT
exit 0
fi
TASKS=$(curl -s \
-H "Authorization: Bearer $TODOIST_API_KEY" \
-H "Content-Type: application/json" \
"https://api.todoist.com/api/v1/tasks?project_id=$PROJECT_ID")
TASK_JSON=$(echo "$TASKS" | python3 -c "
import json, sys
data = json.load(sys.stdin)
tasks = data.get('results', data) if isinstance(data, dict) else data
if not tasks:
print('')
exit()
tasks.sort(key=lambda t: t.get('priority', 1), reverse=True)
t = tasks[0]
prompt = t.get('description', '').strip() or t['content']
print(json.dumps({'id': t['id'], 'content': t['content'], 'prompt': prompt}))
")
if [ -z "$TASK_JSON" ]; then
echo "No tasks found."
echo "task_found=false" >> $GITHUB_OUTPUT
exit 0
fi
TASK_ID=$(echo "$TASK_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")
TASK_CONTENT=$(echo "$TASK_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['content'])")
TASK_PROMPT=$(echo "$TASK_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['prompt'])")
echo "task_found=true" >> $GITHUB_OUTPUT
echo "task_id=$TASK_ID" >> $GITHUB_OUTPUT
echo "task_content<<EOF" >> $GITHUB_OUTPUT
echo "$TASK_CONTENT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
echo "task_prompt<<EOF" >> $GITHUB_OUTPUT
echo "$TASK_PROMPT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Claude Codeをインストール
if: steps.todoist.outputs.task_found == 'true'
run: npm install -g @anthropic-ai/claude-code
- name: タスクに対してClaude Codeを実行
if: steps.todoist.outputs.task_found == 'true'
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
TASK_PROMPT: ${{ steps.todoist.outputs.task_prompt }}
run: |
echo "$TASK_PROMPT" | claude -p \
--dangerously-skip-permissions \
--max-turns 10 \
--output-format stream-json \
--verbose 2>&1 | tee claude-output.log
- name: Claudeが行った変更をコミットする
if: steps.todoist.outputs.task_found == 'true'
run: |
git config user.name "Claude Agent"
git config user.email "claude-agent@users.noreply.github.com"
git add -A
if git diff --staged --quiet; then
echo "No changes to commit."
else
git commit -m "chore: agent task - ${{ steps.todoist.outputs.task_content }}"
git push
fi
- name: Todoistでタスクを完了としてマークする
if: steps.todoist.outputs.task_found == 'true' && success()
env:
TODOIST_API_KEY: ${{ secrets.TODOIST_API_KEY }}
TASK_ID: ${{ steps.todoist.outputs.task_id }}
run: |
curl -s -X POST \
-H "Authorization: Bearer $TODOIST_API_KEY" \
-H "Content-Type: application/json" \
"https://api.todoist.com/api/v1/tasks/$TASK_ID/close"
echo "Task marked complete."
これを作成するか、Linear/Jira/Notion向けに改造する場合は、コメントで教えてください。どんなバリエーションが出てくるかぜひ見てみたいです。
この手の内容をもっと見るために、DEVでは@eli_coding、Instagramでは@eli_codingをフォローしてください。
