Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .agents/skills/calsync-setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,13 @@ curl -s -H "Authorization: Bearer $TOKEN" \

## Step 6: 常駐化

- 推奨(配布形態): `docker compose up -d --build`。設定は `./data/calsync.yaml`、TZ は compose の `TZ` 環境変数(reconcile_at の解釈に使われる)
- コンテナ稼働中の状態確認は `docker compose logs` と `docker exec calsync /calsync status --config /data/calsync.yaml --data /data`。**コンテナ稼働中はホストから data/ の SQLite に一切触れない(読み取りの sqlite3 も禁止 — WAL は VM 境界を跨げず DB 破損の実績あり)。書き込み系コマンドはコンテナ停止中のみ**
- アカウントを後から追加する場合: 認可(Step 4)はホストで行い、コンテナを再起動
常駐方式はユーザーの OS で分岐する。**先にどちらか確認すること**。

- **macOS**: `launchd` によるネイティブ常駐を推奨する(Docker Desktop の VM/自動更新起因の停止を避けられる)。`./scripts/macos/install-launchd.sh` を実行するだけ(冪等。ビルド → plist 生成 → 登録 → 起動まで自動)。トークンを変更したときも同じスクリプトを再実行する。アンインストールは `./scripts/macos/uninstall-launchd.sh`(バイナリ・data/ は残る)。詳細は README「macOS ネイティブ常駐(推奨)」節を参照
- この場合、同一ホストの `status` / `doctor`(OpenReadOnly)はデーモン稼働中でも安全(VirtioFS 境界が存在しないため)。書き込み系コマンド(`sync` / `reconcile` / `accounts remove`)はデーモン停止中のみ、という制約はコンテナ運用と共通
- **Linux / その他(標準)**: `docker compose up -d --build`。設定は `./data/calsync.yaml`、TZ は compose の `TZ` 環境変数(reconcile_at の解釈に使われる)
- コンテナ稼働中の状態確認は `docker compose logs` と `docker exec calsync /calsync status --config /data/calsync.yaml --data /data`。**コンテナ稼働中はホストから data/ の SQLite に一切触れない(読み取りの sqlite3 も禁止 — WAL は VM 境界を跨げず DB 破損の実績あり)。書き込み系コマンドはコンテナ停止中のみ**
- アカウントを後から追加する場合: 認可(Step 4)はホストで行い、常駐プロセス(launchd または コンテナ)を再起動

## Step 7: Slack 通知のセットアップ(オプション)

Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,5 @@ docker compose config -q # compose 構文チェック
- GCP OAuth 同意画面が Testing のままだと refresh token が7日失効 → External は必ず「In production」に publish
- Google Workspace 管理対象アカウントは未検証アプリ+機微スコープを強制ブロック(個人アカウントと違いクリックスルー不可)→ 管理コンソールの「API の制御 → アプリのアクセス制御」で client ID を「信頼できる」に登録
- Entra アプリ登録直後は MSA(login.live.com)への伝播ラグで `invalid_request` が出ることがある → 数分待って再試行
- **コンテナ稼働中、ホストから data/ の SQLite に一切アクセスしない(読み取りも禁止)**。WAL の共有メモリ/ロックは VirtioFS 境界を跨いで機能せず、ホスト側 sqlite3 の読み取りだけで DB が物理破損した実績がある(2026-07-04)。稼働中の状態確認は `docker compose logs` と `docker exec calsync /calsync status --config /data/calsync.yaml --data /data` を使う。flock も境界を跨いで効かないため、ホストの書き込み系コマンドはコンテナ停止中のみ
- **(コンテナ運用時)コンテナ稼働中、ホストから data/ の SQLite に一切アクセスしない(読み取りも禁止)**。WAL の共有メモリ/ロックは VirtioFS 境界を跨いで機能せず、ホスト側 sqlite3 の読み取りだけで DB が物理破損した実績がある(2026-07-04)。稼働中の状態確認は `docker compose logs` と `docker exec calsync /calsync status --config /data/calsync.yaml --data /data` を使う。flock も境界を跨いで効かないため、ホストの書き込み系コマンドはコンテナ停止中のみ。**macOS ネイティブ運用(launchd)では VirtioFS 境界自体が存在しないため、同一ホストの `status` / `doctor`(OpenReadOnly)はデーモン稼働中でも安全**。書き込み系コマンド(`sync` / `reconcile` / `accounts remove`)はネイティブ・コンテナ問わずデーモン停止中のみ、という制約は共通
- DB を失っても `calsync reconcile` がタグから全再構築する(フェーズ0が mappings を先行復元)。再構築系の変更をするときは「Graph delta はタグを返せない」前提を絶対に崩さないこと
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

### Added

- **macOS ネイティブ常駐(launchd)**: Docker Desktop の VM/自動更新起因の停止を避けるため、calsync バイナリを `launchd` の LaunchAgent として直接常駐できるように(`scripts/macos/install-launchd.sh` / `uninstall-launchd.sh` / plist テンプレート)。Go コードの変更なし。Linux / Docker 運用は変更なし
- **通知専用カレンダー(`digest_calendars`)**: ブロッカー配布せずダイジェストにだけ載せるカレンダーを指定可能に(google のみ・リマインド対象外)
- **Slack 通知の Block Kit 化(v2)**: ダイジェストを予定ごとのブロック表示にし、件名をカレンダーの当該予定へのリンクに、Zoom / Meet / Teams の会議 URL を「参加」ボタンにした(conferenceData / onlineMeeting → location・本文の URL 検出の順で抽出)。開始前リマインドに会議参加ボタンと本文全文(プレーンテキスト化・3,000 字制限内に切り詰め)を追加。通知プレビューには従来のテキスト形式を fallback として維持し、blocks が不正な場合はテキストのみで 1 回縮退再送する
- **Slack 通知 v2.1(実表示フィードバック反映)**: ダイジェストの予定行を予定ごとの色付き attachment(`accounts` の定義順で固定パレットを巡回割当、未知アカウントは灰色)に変更し、表示上限を 46 件から 20 件に短縮(超過は「他 N 件」)。リマインドも単一 attachment 化。`chat.postMessage` に `unfurl_links` / `unfurl_media` を常に付与し htmlLink・会議 URL のプレビュー展開を抑止。縮退再送のトリガーに `invalid_attachments` を追加
Expand Down
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,64 @@ docker compose run --rm --entrypoint /calsync calsync \

## 起動

常駐方式は OS によって使い分けます。**macOS では launchd によるネイティブ常駐が推奨**です。Linux やリモートサーバーでは従来どおり Docker を使います。

### macOS ネイティブ常駐(推奨)

Docker Desktop(VM + GUI アプリ + 自動更新)は不安定要素が多く、純 Go・CGO なしの単一バイナリである calsync にとってコンテナの利点はほとんどありません。macOS では `launchd` の LaunchAgent として直接常駐させます。

```bash
./scripts/macos/install-launchd.sh
```

このスクリプトは冪等です(何度実行してもビルド → plist 再生成 → 再登録 → 再起動をやり直すだけ)。内部で行うこと:

1. macOS であること・データディレクトリ(既定 `./data`)・`calsync.yaml` の存在を確認。**コンテナ運用から移行する場合は `calsync.yaml` の `credentials_file` をコンテナ内パス(`/data/google-client.json`)からホストの絶対パスに変更しておくこと**(スクリプトが検知してエラーにします — 実測でクラッシュループになった移行時の落とし穴)
2. `calsync.yaml` に `notifications` が設定されていれば、`bot_token_env`(既定 `SLACK_BOT_TOKEN`)の環境変数が現在のシェルに存在することを確認(なければエラーで中断)
3. `go build` でバイナリを `~/.local/bin/calsync` に配置(`CALSYNC_BIN` で上書き可)
4. Docker の calsync コンテナが稼働中なら中断し、`docker compose down` を案内(移行時の二重運用事故防止。docker コマンドが無い/デーモン停止中は黙ってスキップ)
5. `scripts/macos/com.btajp.calsync.plist.template` からプレースホルダを置換して `~/Library/LaunchAgents/com.btajp.calsync.plist` を生成(`chmod 600` — トークンを含むため)
6. `launchctl bootout` → `bootstrap` → `kickstart` で登録・起動
7. `launchctl print` の起動状態と、ログ(`~/Library/Logs/calsync.log`)末尾を表示

環境変数による上書き:

| 変数 | 内容 | 既定値 |
| --- | --- | --- |
| `CALSYNC_BIN` | バイナリの配置ディレクトリ | `~/.local/bin` |
| `CALSYNC_DATA` | データディレクトリ(`calsync.yaml` の場所) | `<リポジトリ>/data` |

**更新フロー**: `git pull` してから `./scripts/macos/install-launchd.sh` を再実行するだけです(再ビルド・plist 再生成・再起動まで冪等にやり直します)。**トークンを変更した場合も同じスクリプトを再実行**してください(plist に転記し直されます)。

**スリープ挙動**: Mac がスリープ中は同期・通知とも停止し、起床後の最初の tick で再開します。`morning_digest` の時刻にスリープしていた場合、**同日中に起床すれば遅延送信されます**(日付を跨いだ分は放棄される既存仕様どおりです)。

状態確認・アンインストール:

```bash
launchctl print gui/$(id -u)/com.btajp.calsync # 稼働状態
tail -f ~/Library/Logs/calsync.log # ログ
./scripts/macos/uninstall-launchd.sh # アンインストール(バイナリ・data/ は残る)
```

`calsync status` / `calsync doctor` はデータディレクトリの排他ロックを取得しない読み取り専用コマンドなので、ネイティブ運用では**デーモン稼働中でもホストからそのまま実行できます**(コンテナ運用時の制約とは異なります。詳細は [CLAUDE.md](CLAUDE.md) を参照):

```bash
./calsync status --data ./data
./calsync doctor --config ./data/calsync.yaml --data ./data
```

一方 `calsync sync` / `calsync reconcile` / `calsync accounts remove` はデータディレクトリの排他ロックを取得するため、実行前にデーモンを停止してください:

```bash
launchctl bootout gui/$(id -u)/com.btajp.calsync
./calsync reconcile --config ./data/calsync.yaml --data ./data
./scripts/macos/install-launchd.sh # 再起動(再ビルドと再登録も兼ねる)
```

### Linux / その他(標準)

macOS 以外(および macOS 上での動作確認・検証用途)では Docker Compose で常駐します。calsync 本体への変更は一切ないため、この節の手順は従来どおりです。

```bash
docker compose up -d --build
docker compose logs -f calsync
Expand Down
73 changes: 73 additions & 0 deletions docs/superpowers/specs/2026-07-07-macos-launchd-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# calsync macOS ネイティブ常駐(launchd)設計書

作成日: 2026-07-07
ステータス: 承認済みドラフト(実装計画の入力)
関連: [2026-07-03-calsync-design.md](2026-07-03-calsync-design.md)(v1 全体設計)

## 1. 概要・動機

macOS では Docker Desktop 経由の常駐をやめ、**calsync バイナリを launchd の LaunchAgent として直接常駐**できるようにする。Linux では従来どおり Docker / compose 運用を維持する(本機能で Docker サポートは一切変更しない)。

動機(2026-07-06〜07 実障害): Docker Desktop の自動アップデートで仮想ネットワークが壊れ、calsync が 16 時間無言停止した。macOS の Docker Desktop は「VM+GUI アプリ+自動更新」という不安定要素の積み重ねであり、純 Go・CGO なし・単一静的バイナリの calsync にとってコンテナの利点はほぼない。ネイティブ化で以下が根本解消する:

- Docker Desktop / VM 境界起因の停止(今回の障害クラス)
- VirtioFS 境界の SQLite 破損リスク(CLAUDE.md 既知の落とし穴 — ネイティブでは flock / WAL が同一ホスト内で正しく機能する)
- スリープ復帰: launchd 常駐ならプロセスは起床後の最初の tick で継続し、当日分のダイジェストは遅延送信される(nextAt 方式の `now >= next` 判定は wall clock のため。日付を跨いだ分は既存仕様どおり放棄)

**Go コードの変更はゼロ**。追加するのはインストールスクリプト・plist テンプレート・ドキュメントのみ。

## 2. 成果物

| ファイル | 内容 |
| --- | --- |
| `scripts/macos/com.btajp.calsync.plist.template` | LaunchAgent テンプレート(プレースホルダ置換方式) |
| `scripts/macos/install-launchd.sh` | ビルド → 配置 → plist 生成 → `launchctl bootstrap` まで行う冪等スクリプト |
| `scripts/macos/uninstall-launchd.sh` | `launchctl bootout` + plist 削除(バイナリ・data/ は残す) |
| README / CLAUDE.md / calsync-setup スキル / CHANGELOG | 運用手順と注意の更新 |

## 3. 設計

### 3.1 LaunchAgent(plist)

- ラベル: `com.btajp.calsync`。配置先: `~/Library/LaunchAgents/com.btajp.calsync.plist`(**chmod 600** — トークンを含むため)
- `ProgramArguments`: `<BIN>/calsync run --config <DATA>/calsync.yaml --data <DATA>`(すべて絶対パス)
- `RunAtLoad: true` + `KeepAlive: true` — ログイン時自動起動+クラッシュ自動再起動(`restart: unless-stopped` 相当)
- `EnvironmentVariables`: `SLACK_TOKEN_CALSYNC`(または `bot_token_env` で指定した名前)を**plist に直接埋め込む**。launchd の LaunchAgent はシェルプロファイルを読まないため env はここで渡すしかない。ファイルは 600 で保護し、インストールスクリプトが現在のシェル環境変数から値を転記する
- `TZ` は指定しない(システムのタイムゾーンを継承 — `reconcile_at` / `morning_digest` はシステム TZ で解釈される。compose の `TZ: Asia/Tokyo` と等価になることを README に明記)
- ログ: `StandardOutPath` / `StandardErrorPath` → `~/Library/Logs/calsync.log`(単一ファイルに集約)
- `ThrottleInterval: 10`(クラッシュループ時の再起動間隔)

### 3.2 install-launchd.sh(冪等)

1. 前提チェック: macOS であること / リポジトリルートで実行 / `bot_token_env` の環境変数が現在のシェルに存在すること(なければ名前を表示してエラー)
2. `go build -o ~/.local/bin/calsync ./cmd/calsync`(配置先は `CALSYNC_BIN` で上書き可。リポジトリ内の作業ツリーを指さない — checkout に影響されない安定パスに置く)
3. data ディレクトリ(既定 `<repo>/data`、`CALSYNC_DATA` で上書き可)の存在確認
4. **稼働中の Docker コンテナがあれば警告して中断**(`docker compose ps` が calsync を返す場合。二重起動は flock で防がれるが、明示的に「先に `docker compose down` せよ」と案内する — 移行事故防止)
5. テンプレートからプレースホルダ(`__BIN__` / `__DATA__` / `__TOKEN_ENV_NAME__` / `__TOKEN_VALUE__` / `__LOG__`)を置換して plist 生成・600 で配置
6. `launchctl bootout gui/$UID/com.btajp.calsync 2>/dev/null || true` → `launchctl bootstrap gui/$UID <plist>` → `launchctl kickstart gui/$UID/com.btajp.calsync`
7. 起動確認(`launchctl print gui/$UID/com.btajp.calsync` の state と、ログ末尾のエラー有無)を表示して終了

更新フロー(README に明記): `git pull` → `install-launchd.sh` 再実行(再ビルド+plist 再生成+再起動まで冪等にやり直す)。

### 3.3 運用上の注意(ドキュメントに明記)

- **移行**: `docker compose down` → `install-launchd.sh`。data/ は同一ファイルをそのまま使う(単一プロセスの flock 前提は維持される)
- CLAUDE.md の「コンテナ稼働中はホストから SQLite に触るな」は**コンテナ運用時の注意**である旨に更新(ネイティブ運用では同一ホストの `status` / `doctor`(OpenReadOnly)は安全 — 従来どおり。書き込み系はデーモン停止中のみ、は共通)
- スリープ: Mac がスリープ中は同期・通知とも停止し、起床後の最初の tick で再開する。`morning_digest` 時刻にスリープしていた場合、**同日中に起床すれば遅延送信される**(日付を跨いだら既存仕様どおり放棄)
- トークンを変更した場合は `install-launchd.sh` を再実行(plist に転記し直す)
- アンインストール: `uninstall-launchd.sh`(データは消さない)

## 4. テスト計画

Go コード変更ゼロのため単体テストは不要。検証は実機で行う:

1. `bash -n` による両スクリプトの構文チェック+`shellcheck`(あれば)
2. 実機で install → `launchctl print` で running 確認 → `calsync status`(OpenReadOnly)で全アカウント ok → Slack テスト発火(ダイジェスト一時時刻)
3. `kill` でプロセスを落とし、KeepAlive による自動再起動を確認
4. uninstall → プロセス消滅と plist 削除を確認 → 再 install(冪等性)

## 5. スコープ外

- Linux の systemd unit(需要が出たら同型で追加可能)
- Apple Container 対応(VM 境界が残るため見送り — 検討記録として本スペックに残す)
- Docker サポートの変更(一切なし。Linux とリモート運用の標準は compose のまま)
Loading