From 9136ea6c35d803c3ac99a406b4a1d776b2b55043 Mon Sep 17 00:00:00 2001 From: masseater Date: Mon, 6 Apr 2026 10:02:37 +0900 Subject: [PATCH 1/2] docs: add ops-harbor automation pipeline design spec Design spec for replacing the single-runner automation system with a Typed Phase Pipeline + ActionExecutor architecture. Covers pipeline phases, type system, provider abstraction, user configuration, DDD domain model, FSD module structure, and scalability considerations. Co-Authored-By: Claude Opus 4.6 (1M context) --- ...6-ops-harbor-automation-pipeline-design.md | 415 ++++++++++++++++++ 1 file changed, 415 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-06-ops-harbor-automation-pipeline-design.md diff --git a/docs/superpowers/specs/2026-04-06-ops-harbor-automation-pipeline-design.md b/docs/superpowers/specs/2026-04-06-ops-harbor-automation-pipeline-design.md new file mode 100644 index 00000000..e15c1934 --- /dev/null +++ b/docs/superpowers/specs/2026-04-06-ops-harbor-automation-pipeline-design.md @@ -0,0 +1,415 @@ +# ops-harbor Automation Pipeline Design + +## Overview + +ops-harbor の自動化機能を、Typed Phase Pipeline + ActionExecutor\ パターンで再設計する。 +GitHub PR イベントを受信し、ルールベースでトリガーを評価し、AI ツール (claude-code, codex) や GitHub API を通じて自動アクションを実行する。 + +## Design Decisions + +### Architecture: Plan C' (ActionExecutor\ + ExecutorRegistry) + +6人のチーム議論 (pipeline-architect, use-case-designer, devils-advocate, scalability-reviewer, ddd-specialist, fsd-specialist) を経て合意。 + +**採用した設計要素:** +- Typed Phase Pipeline (6フェーズ、明示的フェーズ関数、next() なし) +- ActionExecutor\ (kind ごとに型安全な実行器) +- ExecutorRegistry (Required mapped type、登録漏れをコンパイルエラーに) +- CliRunnerConfig with argsTemplate (JSON config で CLI ツール追加可能) +- Alert と Trigger の概念分離 + +**棄却した設計要素:** +- Koa-style `(ctx, next) => void` — HTTP request-response ではなく線形データエンリッチメント +- AiRunner interface — Rule of Three 未達。関数で十分 +- ApiExecutor interface — 2系統分割は update_branch (merge=API, rebase=AI) で破綻 +- SideEffect accumulator — Action[] 逐次実行の方が透明 +- ActionProvider (full union execute) — ActionExecutor\ で置き換え +- `__api__` sentinel — magic string + +## Pipeline Architecture + +### 6 Phases + +``` +ingest → enrich → evaluate → plan → execute → finalize +``` + +各フェーズが型付き入力を受け取り、型付き出力を返す。線形。 + +| Phase | 入力 | 出力 | 責務 | +|-------|------|------|------| +| ingest | RawWebhook | IngestedEvent[] | Webhook parse, dedup, fan-out (push→N PRs) | +| enrich | IngestedEvent | EnrichedContext | WorkItem hydrate, recent events 取得 | +| evaluate | EnrichedContext | EvaluatedContext | TriggerRule 実行 → Trigger[] 生成 | +| plan | EvaluatedContext | PlannedContext | Trigger → Action mapping (config bindings) | +| execute | PlannedContext | ExecutedContext | ActionExecutor dispatch → ActionResult[] | +| finalize | ExecutedContext | void | DB 記録、通知、cleanup | + +### DDD Mapping + +| Phase | DDD Pattern | +|-------|-------------| +| ingest | Anti-Corruption Layer | +| enrich | Read Model 補完 | +| evaluate | Domain Service (TriggerRule) | +| plan | Domain Service (Trigger → Action) | +| execute | Application Service | +| finalize | 永続化 + 副作用 | + +## Core Types + +### Event / Context (段階的に型が広がる intersection type) + +```typescript +type IngestedEvent = { + source: "webhook" | "cron" | "manual"; + eventType: string; + deliveryId: string; + repository: string; + payload: Record; + affectedPullNumbers: number[]; +}; + +type EnrichedContext = IngestedEvent & { + workItem: WorkItem; + recentEvents: ActivityEvent[]; +}; + +type EvaluatedContext = EnrichedContext & { + triggers: Trigger[]; +}; + +type PlannedContext = EvaluatedContext & { + actions: Action[]; +}; + +type ExecutedContext = PlannedContext & { + results: ActionResult[]; +}; +``` + +### Trigger (discriminated union) + +```typescript +type TriggerKind = + | "ci_failed" + | "conflicted" + | "base_behind" + | "review_changes_requested" + | "review_commented" + | string; // escape hatch for future triggers + +type Trigger = + | { kind: "ci_failed"; failedChecks: string[]; headSha: string } + | { kind: "conflicted"; baseBranch: string; headBranch: string } + | { kind: "base_behind"; baseBranch: string } + | { kind: "review_changes_requested"; reviewer: string; body: string } + | { kind: "review_commented"; reviewer: string; body: string } + | { kind: string; [key: string]: unknown }; +``` + +Note: `TriggerKind` は旧 `AutomationTrigger` のリネーム。DDD 的に「種別」であり「イベントそのもの」ではない。 + +### Alert vs Trigger の分離 + +- **Alert**: WorkItem の状態の問題点。UI 表示用 (dashboard)。severity を持つ +- **Trigger**: パイプラインが反応すべきイベント。automation 用。Action に対応する + +現行コードでは `AutomationTrigger` が両方を兼ねている。これを分離する。 + +### Action (discriminated union) + +```typescript +type ActionKind = "run_ai_fix" | "update_branch" | "post_comment" | "notify"; + +type Action = + | { kind: "run_ai_fix"; prompt: string; workDir: string; tool: string } + | { kind: "update_branch"; owner: string; repo: string; pullNumber: number; strategy: "merge" | "rebase" } + | { kind: "post_comment"; owner: string; repo: string; pullNumber: number; body: string } + | { kind: "notify"; channel: string; message: string }; +``` + +### ActionResult + +```typescript +type ActionResult = { + action: Action; + status: "completed" | "failed" | "skipped"; + summary: string; + durationMs: number; + followUpActions?: Action[]; +}; +``` + +`followUpActions` により AI fix → post comment の sequential パターンを表現。 + +### TriggerRule (pure function, Domain Service) + +```typescript +type TriggerRule = { + id: string; + evaluate(ctx: EnrichedContext): Trigger | null; +}; +``` + +- 副作用なし、テスト容易 +- 実装は app 層に配置 (core ではない — FSD) + +### ActionExecutor\ + ExecutorRegistry + +```typescript +type ActionExecutor = { + kind: K; + execute: ( + action: Extract, + ctx: Readonly, + signal: AbortSignal, + ) => Promise; +}; + +// Required — ActionKind 追加時に登録漏れでコンパイルエラー +type ExecutorRegistry = { [K in ActionKind]: ActionExecutor }; + +// Dispatch: 1箇所の `as never` cast (TypeScript correlated union limitation) +function dispatchAction( + registry: ExecutorRegistry, + action: Action, + ctx: Readonly, + signal: AbortSignal, +): Promise { + const executor = registry[action.kind as ActionKind]; + return executor.execute(action as never, ctx, signal); +} +``` + +`as never` は TypeScript の correlated union 制限 (microsoft/TypeScript#30581) による。dispatch 関数1箇所に限定。 + +## Action Providers + +### claude-code Provider + +```typescript +// ActionExecutor<"run_ai_fix"> の内部実装 +// claude -p "prompt" --permission-mode auto -C /path を spawn +``` + +### codex Provider + +```typescript +// ActionExecutor<"run_ai_fix"> の内部実装 +// codex exec "prompt" --full-auto -C /path を spawn +``` + +### github-api Provider + +```typescript +// ActionExecutor<"update_branch"> の内部実装 +// PUT /repos/{owner}/{repo}/pulls/{number}/update-branch +``` + +### CLI Tool Config (JSON config で追加可能) + +```typescript +type CliRunnerConfig = { + name: string; + command: string; + argsTemplate: string[]; // ["--prompt", "{{prompt}}", "-C", "{{workDir}}"] + parseOutput: "json" | "plain" | "none"; + env?: Record; +}; +``` + +builtin ツール (claude, codex) は hardcoded config。custom CLI は UserConfig から変換。 + +### 拡張戦略 + +- 新 CLI ツール: config の `tools[]` に追加 (zero code change) +- Devin (API polling): `spawnCliTool` を interface に昇格 (Rule of Three 発動時) +- 新 Action 種: `ActionKind` union member + `ActionExecutor` 追加 + +## User Configuration + +```json +{ + "tools": [ + { "name": "claude", "type": "builtin" }, + { "name": "codex", "type": "builtin" }, + { "name": "aider", "type": "cli", "command": "aider", + "args": ["--message", "{{prompt}}", "--yes-always"], + "parseOutput": "plain" } + ], + "routing": [ + { "trigger": "ci_failed", "tool": "claude" }, + { "trigger": "ci_failed", "tool": "aider", "repository": "acme/ml-pipeline" }, + { "trigger": "base_behind", "action": "update_branch" }, + { "trigger": "review_changes_requested", "tool": "codex" }, + { "trigger": "review_commented", "tool": "claude" } + ], + "defaultTool": "claude" +} +``` + +- ActionKind はパイプライン内部のみ。config に漏れない +- repository-specific routing で同じ trigger に異なるツールを割り当て可能 + +## Entry Points + +### Webhook (primary) + +GitHub Webhook → ingest → enrich → evaluate → plan → execute → finalize + +### Cron (scheduled) + +Synthetic IngestedEvent (source: "cron") を生成して同じパイプラインを再利用。 +例: `stale_review` (3日間レビューなし) は cron で全 open PR を走査。 + +### Manual + +API endpoint から手動トリガー。source: "manual"。 + +## Fan-out + +push webhook は 1:N のファンアウトが必要。ingest フェーズが `IngestedEvent[]` を返し、各イベントに対して enrich 以降を個別実行。 + +## Deduplication & Concurrency + +- **Webhook dedup**: deliveryId でべき等性 +- **Job dedup**: 同一 (workItemId, triggerKind) に対して 4 時間 dedup window +- **Stale job cancel**: headSha をジョブ作成時に記録。実行前に比較、stale ならスキップ +- **Lease mechanism**: ジョブの直列化ポイント。同一 PR への同時アクションを防止 +- **AbortSignal**: 実行中ジョブのキャンセル対応 + +## Error Handling + +- 各フェーズの実行時にフェーズ名をエラーに付加 (Error wrapping) +- AI provider spawn 失敗 → ActionResult.status = "failed"、summary にエラーメッセージ +- 前のアクションが失敗したら後続はスキップ (sequential execution) +- finalize フェーズで全結果を DB に記録 + +## Scalability Considerations (Priority Order) + +| Priority | Item | Description | +|----------|------|-------------| +| P0 | AbortSignal | 実行中ジョブのキャンセル | +| P0 | headSha stale check | executor 内、spawn 前に1行チェック | +| P1 | Webhook debouncing | 同一 PR の 5s 以内の重複を抑制 | +| P1 | GitHub API caching | ETag/If-None-Match による conditional request | +| P2 | Multi-worker | worker pool + per-PR git worktree | +| P2 | webhook_deliveries cleanup | 7日 TTL | +| P2 | Executor-level concurrency | `concurrency?: number` per ActionKind | +| P3 | PostgreSQL migration path | SQLite single-writer が限界に達した場合 | + +## DDD Domain Model + +| Concept | DDD Pattern | Note | +|---------|-------------|------| +| WorkItem | Read Model / Projection | GitHub が authority。ops-harbor は読み取り専用 | +| AutomationJob | Entity | ops-harbor 唯一の owned entity。id, lifecycle, status 遷移 | +| TriggerKind | Value Object | 不変の識別子 | +| WorkItemAlert | Value Object (derived) | WorkItem 状態からの派生計算値 | +| ActivityEvent | Event | domain + infra 混合 → 将来分離候補 | +| TriggerRule | Domain Service | WorkItem → Trigger[] の純粋評価関数 | + +核心的洞察: ops-harbor は「外部システム(GitHub)の状態を監視し、ルールに基づいて自動アクションを実行する」反応型システム。Authority を持つデータは AutomationJob のみ。 + +## Module Structure (FSD) + +### core (shared layer — pure types only) + +``` +packages/ops-harbor-core/src/ + pipeline/ + types.ts # PipelineContext, Trigger, Action, ActionResult + executor-types.ts # ActionExecutor, ExecutorRegistry + rule-types.ts # TriggerRule interface + types.ts # WorkItem, ActivityEvent, etc. + alerts.ts # Alert derivation (UI 用、Trigger とは別) + filters.ts + db-helpers.ts + sqlite.ts + expand-template.ts # expandTemplate (旧 prompt.ts から分離) + port-finder.ts +``` + +Note: `buildAutomationPrompt` は core から app 層に移動。`expandTemplate` のみ core に残す。TriggerRule の実装 (ci-failed.ts 等) も app 層。 + +### app (business logic + execution) + +``` +apps/ops-harbor/src/ + pipeline/ + ingest.ts + enrich.ts + evaluate.ts # builtin TriggerRule 実装もここ + plan.ts # Trigger → Action mapping, prompt 構築 + execute.ts # ExecutorRegistry 経由で dispatch (provider を直接 import しない) + finalize.ts + runner.ts # 全フェーズを直列実行する orchestrator + rules/ + ci-failed.ts + conflicted.ts + base-behind.ts + review.ts + providers/ + claude-code.ts + codex.ts + github-api.ts + cli-tool-runner.ts # 汎用 CLI spawn + config.ts # PipelineConfig schema (tools, routing, defaultTool) + server.ts # Composition root — provider と rule の wiring +``` + +### control-plane (GitHub integration + job queue) + +``` +apps/ops-harbor-control-plane/src/ + lib/ + db/ + schema.ts + work-items.ts + activity.ts + jobs.ts # enqueueJobsForAlerts, leaseNextAutomationJob + webhooks.ts + rate-limits.ts + index.ts + github/ + auth.ts # JWT, installation token + graphql.ts # queries + client.ts # HTTP client + mapping.ts # PR → WorkItem 変換 + webhook-verify.ts + update-branch.ts # PUT /repos/.../update-branch + index.ts + config.ts + sync.ts + tunnel.ts + server.ts +``` + +### Composition Root Pattern + +`server.ts` が唯一の wiring point。Pipeline に TriggerRule[] と ExecutorRegistry を注入。execute.ts は ExecutorRegistry のみ参照し、具体的な provider を知らない。 + +## Naming Changes + +| Current | New | Reason | +|---------|-----|--------| +| AutomationTrigger | TriggerKind | DDD: 種別であってイベントではない | +| StatusTone | StatusLevel | より直感的 | +| runner (config) | tools + routing | 単一コマンドから複数ツール対応に | +| buildAutomationPrompt | (app 層の plan phase に移動) | ビジネスロジックは core に置かない | + +## Cross-boundary Actions + +`update_branch` の strategy="merge" は GitHub API、strategy="rebase" は AI spawn。 +1つの `ActionExecutor<"update_branch">` 内で分岐。案B の 2 interface split では表現不能だった問題を解決。 + +## Executor Responsibilities + +`ActionExecutor<"run_ai_fix">` が以下の全サイクルをオーケストレーション: +1. headSha stale check +2. CLI tool spawn (claude/codex/custom) +3. 結果検証 (validateCommand) +4. commit & push (pushPolicy に従う) + +Middleware フェーズ分割ではなく executor 内に閉じ込める。 From 40f2da9a3beebd0adbd76ee83231667b1b7164ea Mon Sep 17 00:00:00 2001 From: masseater Date: Mon, 6 Apr 2026 10:36:06 +0900 Subject: [PATCH 2/2] docs: refine pipeline design spec based on Codex review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address all P0/P1/P2 findings from Codex architecture review: - Close TriggerKind union (remove `| string` escape hatch) with boundary validation - Fix 6 internal inconsistencies (argsTemplate/args, config ActionKind leak, etc.) - Correct DDD labels (plan → Application Service, ActivityEvent → Domain Event) - Define Alert/Trigger derivation relationship and notify_user vs finalize split - Add Phase type constraint, PhaseError, retry policy, and failure propagation - Promote rate limiting to P0 and per-PR worktree to P1 in scalability table Co-Authored-By: Claude Opus 4.6 (1M context) --- ...6-ops-harbor-automation-pipeline-design.md | 208 ++++++++++++++---- 1 file changed, 161 insertions(+), 47 deletions(-) diff --git a/docs/superpowers/specs/2026-04-06-ops-harbor-automation-pipeline-design.md b/docs/superpowers/specs/2026-04-06-ops-harbor-automation-pipeline-design.md index e15c1934..75b17888 100644 --- a/docs/superpowers/specs/2026-04-06-ops-harbor-automation-pipeline-design.md +++ b/docs/superpowers/specs/2026-04-06-ops-harbor-automation-pipeline-design.md @@ -41,23 +41,43 @@ ingest → enrich → evaluate → plan → execute → finalize | ingest | RawWebhook | IngestedEvent[] | Webhook parse, dedup, fan-out (push→N PRs) | | enrich | IngestedEvent | EnrichedContext | WorkItem hydrate, recent events 取得 | | evaluate | EnrichedContext | EvaluatedContext | TriggerRule 実行 → Trigger[] 生成 | -| plan | EvaluatedContext | PlannedContext | Trigger → Action mapping (config bindings) | +| plan | EvaluatedContext | PlannedContext | Trigger → Action mapping (config routing + 順序計画) | | execute | PlannedContext | ExecutedContext | ActionExecutor dispatch → ActionResult[] | -| finalize | ExecutedContext | void | DB 記録、通知、cleanup | +| finalize | ExecutedContext | void | DB 記録、運用通知 (job 完了/失敗)、cleanup | + +**通知の責務分離**: +- **`notify_user` Action** (plan → execute): ユーザー向け業務通知。例: 「CI 修正を自動実行しました」 +- **finalize の運用通知**: システム運用向け。ジョブ完了/失敗のログ記録、監視チャネルへの通知。ユーザー設定不要 ### DDD Mapping -| Phase | DDD Pattern | -|-------|-------------| -| ingest | Anti-Corruption Layer | -| enrich | Read Model 補完 | -| evaluate | Domain Service (TriggerRule) | -| plan | Domain Service (Trigger → Action) | -| execute | Application Service | -| finalize | 永続化 + 副作用 | +| Phase | DDD Pattern | Note | +|-------|-------------|------| +| ingest | Anti-Corruption Layer | 外部 Webhook を内部モデルに変換。未知入力の reject | +| enrich | Read Model 補完 | GitHub API から WorkItem を hydrate | +| evaluate | Domain Service (TriggerRule) | 純粋な状態評価。副作用なし | +| plan | Application Service (Policy Mapping) | config/routing を読み Trigger → Action[] に変換。ビジネスルールではなく運用ポリシー | +| execute | Application Service (Orchestration) | ExecutorRegistry 経由で dispatch | +| finalize | Infrastructure (永続化 + 運用通知) | DB 記録、監視チャネル通知 | ## Core Types +### Phase 型制約 + +```typescript +type Phase = (input: I, signal: AbortSignal) => Promise; + +// 各フェーズ関数はこの型に準拠する +type IngestPhase = Phase; +type EnrichPhase = Phase; +type EvaluatePhase = Phase; +type PlanPhase = Phase; +type ExecutePhase = Phase; +type FinalizePhase = Phase; +``` + +`Phase` により、フェーズ間の入出力型の接続がコンパイル時に検証される。runner がフェーズを直列合成する際の型安全性を保証する。 + ### Event / Context (段階的に型が広がる intersection type) ```typescript @@ -88,7 +108,7 @@ type ExecutedContext = PlannedContext & { }; ``` -### Trigger (discriminated union) +### Trigger (closed discriminated union) ```typescript type TriggerKind = @@ -96,20 +116,34 @@ type TriggerKind = | "conflicted" | "base_behind" | "review_changes_requested" - | "review_commented" - | string; // escape hatch for future triggers + | "review_commented"; type Trigger = | { kind: "ci_failed"; failedChecks: string[]; headSha: string } | { kind: "conflicted"; baseBranch: string; headBranch: string } | { kind: "base_behind"; baseBranch: string } | { kind: "review_changes_requested"; reviewer: string; body: string } - | { kind: "review_commented"; reviewer: string; body: string } - | { kind: string; [key: string]: unknown }; + | { kind: "review_commented"; reviewer: string; body: string }; ``` Note: `TriggerKind` は旧 `AutomationTrigger` のリネーム。DDD 的に「種別」であり「イベントそのもの」ではない。 +**Closed union の設計判断**: `| string` escape hatch は discriminated union の網羅性チェック(exhaustive switch)を無効にするため採用しない。新 trigger 追加時は `TriggerKind` に union member を追加し、plan/routing の対応漏れをコンパイルエラーで検出する。外部 config からの未知 trigger 文字列は ingest 境界で `UnknownTriggerInput` として reject する: + +```typescript +// Boundary validation (ingest phase) +type RawTriggerInput = string; + +function parseTriggerKind(raw: RawTriggerInput): TriggerKind { + const valid: Set = new Set([ + "ci_failed", "conflicted", "base_behind", + "review_changes_requested", "review_commented", + ]); + if (!valid.has(raw)) throw new UnknownTriggerError(raw); + return raw as TriggerKind; +} +``` + ### Alert vs Trigger の分離 - **Alert**: WorkItem の状態の問題点。UI 表示用 (dashboard)。severity を持つ @@ -117,16 +151,30 @@ Note: `TriggerKind` は旧 `AutomationTrigger` のリネーム。DDD 的に「 現行コードでは `AutomationTrigger` が両方を兼ねている。これを分離する。 +**導出関係**: Alert と Trigger は同一の `EnrichedContext` から **独立に** 導出される。 + +``` +EnrichedContext ──→ deriveAlerts() → Alert[] (UI dashboard 用) + └─→ evaluateRules() → Trigger[] (automation pipeline 用) +``` + +- `deriveAlerts()` は WorkItem の現在状態を評価し、表示用の問題点リストを返す(severity 付き) +- `evaluateRules()` は TriggerRule[] を実行し、自動化対象のイベントリストを返す +- 同じ条件(例: CI 失敗)から Alert と Trigger の両方が生成されることがあるが、ライフサイクルは独立 +- Alert は dismiss/acknowledge 可能。Trigger は一度評価されたら Action に変換されて消費される + ### Action (discriminated union) ```typescript -type ActionKind = "run_ai_fix" | "update_branch" | "post_comment" | "notify"; +type ToolName = "claude" | "codex" | (string & {}); // builtin + custom CLI names + +type ActionKind = "run_ai_fix" | "update_branch" | "post_comment" | "notify_user"; type Action = - | { kind: "run_ai_fix"; prompt: string; workDir: string; tool: string } + | { kind: "run_ai_fix"; prompt: string; workDir: string; tool: ToolName } | { kind: "update_branch"; owner: string; repo: string; pullNumber: number; strategy: "merge" | "rebase" } | { kind: "post_comment"; owner: string; repo: string; pullNumber: number; body: string } - | { kind: "notify"; channel: string; message: string }; + | { kind: "notify_user"; channel: string; message: string }; ``` ### ActionResult @@ -137,23 +185,34 @@ type ActionResult = { status: "completed" | "failed" | "skipped"; summary: string; durationMs: number; - followUpActions?: Action[]; }; ``` -`followUpActions` により AI fix → post comment の sequential パターンを表現。 +**Sequential action パターン**: AI fix → post comment のような連鎖は `plan` フェーズで事前に `Action[]` の順序として計画する。execute フェーズは計画された順序で逐次実行するのみ。execute 中に新たな Action を生成しない(plan の責務侵食を防ぐ)。 + +```typescript +// plan phase が生成する Action[] の例 +// [ +// { kind: "run_ai_fix", ... }, +// { kind: "post_comment", body: "{{ai_fix_result}}" }, // テンプレート変数で前段結果を参照 +// ] +``` + +テンプレート変数 `{{ai_fix_result}}` は execute フェーズ内で前段の `ActionResult.summary` に置換される。これにより plan の前計算と execute の逐次実行の責務分離を維持する。 -### TriggerRule (pure function, Domain Service) +### TriggerRule (pure function) ```typescript type TriggerRule = { id: string; - evaluate(ctx: EnrichedContext): Trigger | null; + evaluate(ctx: EnrichedContext): Trigger[]; }; ``` - 副作用なし、テスト容易 -- 実装は app 層に配置 (core ではない — FSD) +- 1つの Rule が 0〜N 個の Trigger を返す(例: CI で複数チェックが失敗 → 複数 Trigger) +- evaluate phase は全 Rule の結果を `flat()` して `EvaluatedContext.triggers` に集約 +- 実装は app 層に配置。理由: TriggerRule は domain の概念(Value Object 的な判定ロジック)だが、具体的な rule(ci-failed, conflicted 等)は ops-harbor アプリ固有のビジネスポリシー。core は interface のみ持ち、実装は app 層で DI する(FSD: shared layer は抽象のみ) ### ActionExecutor\ + ExecutorRegistry @@ -235,13 +294,14 @@ builtin ツール (claude, codex) は hardcoded config。custom CLI は UserConf { "name": "claude", "type": "builtin" }, { "name": "codex", "type": "builtin" }, { "name": "aider", "type": "cli", "command": "aider", - "args": ["--message", "{{prompt}}", "--yes-always"], + "argsTemplate": ["--message", "{{prompt}}", "--yes-always"], "parseOutput": "plain" } ], "routing": [ { "trigger": "ci_failed", "tool": "claude" }, { "trigger": "ci_failed", "tool": "aider", "repository": "acme/ml-pipeline" }, - { "trigger": "base_behind", "action": "update_branch" }, + { "trigger": "base_behind", "strategy": "merge" }, + { "trigger": "conflicted", "strategy": "rebase", "tool": "claude" }, { "trigger": "review_changes_requested", "tool": "codex" }, { "trigger": "review_commented", "tool": "claude" } ], @@ -249,8 +309,12 @@ builtin ツール (claude, codex) は hardcoded config。custom CLI は UserConf } ``` -- ActionKind はパイプライン内部のみ。config に漏れない -- repository-specific routing で同じ trigger に異なるツールを割り当て可能 +**Config の設計原則**: +- `tools[]` は利用可能なツール定義。`argsTemplate` で統一(`args` は使わない) +- `routing[]` は `trigger` → ツール/戦略のマッピング。`ActionKind` はパイプライン内部の実装概念であり config に露出しない +- `trigger` の値は `TriggerKind` の文字列表現。config パース時に `parseTriggerKind()` で検証される +- `strategy` は `update_branch` 系アクションの戦略指定。plan フェーズが routing config から適切な `ActionKind` + パラメータに変換する +- repository-specific routing で同じ trigger に異なるツール/戦略を割り当て可能 ## Entry Points @@ -273,18 +337,47 @@ push webhook は 1:N のファンアウトが必要。ingest フェーズが `In ## Deduplication & Concurrency -- **Webhook dedup**: deliveryId でべき等性 +- **Webhook dedup (ingest)**: deliveryId でべき等性。同一 delivery の再送を除去 +- **Webhook debouncing (ingest)**: 同一 PR の 5s window 内に到着した別 delivery を集約。dedup とは独立した機構 - **Job dedup**: 同一 (workItemId, triggerKind) に対して 4 時間 dedup window - **Stale job cancel**: headSha をジョブ作成時に記録。実行前に比較、stale ならスキップ - **Lease mechanism**: ジョブの直列化ポイント。同一 PR への同時アクションを防止 - **AbortSignal**: 実行中ジョブのキャンセル対応 +- **Rate limit budget**: GitHub API と CLI spawn それぞれに同時実行数上限を設定。executor が budget を超える場合は queue で待機 ## Error Handling -- 各フェーズの実行時にフェーズ名をエラーに付加 (Error wrapping) -- AI provider spawn 失敗 → ActionResult.status = "failed"、summary にエラーメッセージ -- 前のアクションが失敗したら後続はスキップ (sequential execution) -- finalize フェーズで全結果を DB に記録 +### Phase-level エラー記録 + +各フェーズの失敗を統一フォーマットで記録する: + +```typescript +type PhaseError = { + phase: "ingest" | "enrich" | "evaluate" | "plan" | "execute" | "finalize"; + errorType: "transient" | "permanent"; + message: string; + cause?: unknown; + timestamp: string; +}; +``` + +- **transient**: ネットワークタイムアウト、GitHub API rate limit、一時的な spawn 失敗 → retry 対象 +- **permanent**: 不正な Webhook ペイロード、未知の trigger、設定エラー → retry しない + +### Retry 方針 + +| 対象 | 戦略 | 上限 | +|------|------|------| +| GitHub API (5xx, rate limit) | Exponential backoff | 3回、最大 30s | +| CLI tool spawn 失敗 | 即時 1回 retry | 1回 | +| Webhook parse 失敗 (permanent) | retry なし | - | +| Config validation 失敗 (permanent) | retry なし | - | + +### 失敗伝播ルール + +- ingest/enrich/evaluate/plan の失敗 → パイプライン全体を中断。PhaseError を DB に記録 +- execute 内の Action 失敗 → 後続 Action をスキップ。ActionResult.status = "failed" +- finalize の失敗 → stderr ログ出力 + 監視チャネル通知(best-effort)。finalize 自体の失敗でパイプライン結果は変わらない ## Scalability Considerations (Priority Order) @@ -292,46 +385,54 @@ push webhook は 1:N のファンアウトが必要。ingest フェーズが `In |----------|------|-------------| | P0 | AbortSignal | 実行中ジョブのキャンセル | | P0 | headSha stale check | executor 内、spawn 前に1行チェック | -| P1 | Webhook debouncing | 同一 PR の 5s 以内の重複を抑制 | +| P0 | Rate limiting / backpressure | GitHub API rate limit 検出 + CLI spawn 同時実行数制限 | +| P1 | Webhook dedup vs debouncing | **dedup**: deliveryId による再送除去。**debouncing**: 同一 PR の 5s window 内の別 delivery 集約。両者は独立した機構 | | P1 | GitHub API caching | ETag/If-None-Match による conditional request | -| P2 | Multi-worker | worker pool + per-PR git worktree | +| P1 | Per-PR git worktree | execute が commit/push を含むため single-worker でも worktree 分離が必要。`git worktree add` で PR ごとに隔離 | +| P2 | Multi-worker | worker pool。per-PR worktree が前提 | | P2 | webhook_deliveries cleanup | 7日 TTL | -| P2 | Executor-level concurrency | `concurrency?: number` per ActionKind | +| P2 | Executor-level concurrency | `concurrency?: number` per ActionKind。rate limit budget を executor 間で分配 | | P3 | PostgreSQL migration path | SQLite single-writer が限界に達した場合 | +**Single-worker 前提の明示**: 初期実装は single-worker。execute が commit/push を含むため、同一 PR への並行実行は lease mechanism で防止する。multi-worker (P2) 移行時は per-PR worktree + lease が前提となる。 + ## DDD Domain Model | Concept | DDD Pattern | Note | |---------|-------------|------| | WorkItem | Read Model / Projection | GitHub が authority。ops-harbor は読み取り専用 | | AutomationJob | Entity | ops-harbor 唯一の owned entity。id, lifecycle, status 遷移 | -| TriggerKind | Value Object | 不変の識別子 | -| WorkItemAlert | Value Object (derived) | WorkItem 状態からの派生計算値 | -| ActivityEvent | Event | domain + infra 混合 → 将来分離候補 | -| TriggerRule | Domain Service | WorkItem → Trigger[] の純粋評価関数 | +| TriggerKind | Value Object | 不変の識別子。closed union で網羅性を保証 | +| WorkItemAlert | Value Object (derived) | WorkItem 状態からの派生計算値。UI dashboard 用 | +| ActivityEvent | Domain Event | GitHub Webhook 由来のイベント記録 | +| TriggerRule | Domain Service (interface) | EnrichedContext → Trigger[] の純粋評価関数。interface は core、実装は app | +| RoutingPolicy | Application Policy | config/routing から Trigger → Action[] への変換。plan phase の責務 | 核心的洞察: ops-harbor は「外部システム(GitHub)の状態を監視し、ルールに基づいて自動アクションを実行する」反応型システム。Authority を持つデータは AutomationJob のみ。 +**ActivityEvent の位置づけ**: 旧仕様では「domain + infra 混合 → 将来分離候補」としていたが、GitHub Webhook 由来のイベントを内部表現に変換した時点で Domain Event として扱う。ingest phase(ACL)が変換を担うため、enrich 以降では内部モデルとして一貫して使用できる。 + ## Module Structure (FSD) -### core (shared layer — pure types only) +### core (shared layer — types + pure functions) ``` packages/ops-harbor-core/src/ pipeline/ - types.ts # PipelineContext, Trigger, Action, ActionResult + types.ts # PipelineContext, Trigger, Action, ActionResult, Phase executor-types.ts # ActionExecutor, ExecutorRegistry rule-types.ts # TriggerRule interface types.ts # WorkItem, ActivityEvent, etc. alerts.ts # Alert derivation (UI 用、Trigger とは別) - filters.ts - db-helpers.ts - sqlite.ts - expand-template.ts # expandTemplate (旧 prompt.ts から分離) - port-finder.ts + filters.ts # WorkItem filtering (pure) + expand-template.ts # expandTemplate (pure, 旧 prompt.ts から分離) + infra/ + db-helpers.ts # SQLite utility (infra) + sqlite.ts # SQLite connection (infra) + port-finder.ts # Port discovery (infra) ``` -Note: `buildAutomationPrompt` は core から app 層に移動。`expandTemplate` のみ core に残す。TriggerRule の実装 (ci-failed.ts 等) も app 層。 +Note: core は **types + pure functions** と **infra utilities** を含む。`infra/` サブディレクトリで副作用を持つモジュールを分離し、pipeline types との境界を明確にする。`buildAutomationPrompt` は app 層に移動。TriggerRule の実装 (ci-failed.ts 等) も app 層。 ### app (business logic + execution) @@ -398,12 +499,25 @@ apps/ops-harbor-control-plane/src/ | StatusTone | StatusLevel | より直感的 | | runner (config) | tools + routing | 単一コマンドから複数ツール対応に | | buildAutomationPrompt | (app 層の plan phase に移動) | ビジネスロジックは core に置かない | +| notify (ActionKind) | notify_user | finalize の運用通知と区別。ユーザー向け業務通知であることを明示 | ## Cross-boundary Actions `update_branch` の strategy="merge" は GitHub API、strategy="rebase" は AI spawn。 1つの `ActionExecutor<"update_branch">` 内で分岐。案B の 2 interface split では表現不能だった問題を解決。 +**rebase 時の追加情報**: strategy="rebase" の場合、executor は内部で以下を実行する: +1. per-PR worktree を取得 +2. `git rebase` を AI tool (claude/codex) に委譲 +3. conflict 解決後に force-push (pushPolicy に従う) + +Action 型には `tool` と `workDir` を持たないが、executor が `PlannedContext` から PR 情報を参照し、worktree パスと使用ツールを解決する。これにより Action 型をシンプルに保つ。 + +```typescript +// pushPolicy: execute phase の設定 +type PushPolicy = "force-with-lease" | "dry-run"; +``` + ## Executor Responsibilities `ActionExecutor<"run_ai_fix">` が以下の全サイクルをオーケストレーション: