Skip to content

fix(autopilot): 未分解 EPIC への 🧭 tracking 付与が daemon 経路で復活し decompose がデッドロックする #1130

Description

@smalruby3-editor-bot

症状

未分解の EPIC(sub-issue 0 件)に 🧭 tracking が付与され、decompose が永久に走らなくなる。

#1129 で実際に発生した。経緯:

時刻 (UTC) 出来事
07:51:48 decompose を dispatch(EPIC / Sprint Backlog)
07:54:11 decompose が分解案を提示して 🙋 HITL で承認待ち(Status=In Progress / AI Status=Decomposing のまま)
07:57:17 daemon が 🧭 tracking を付与(sub-issue は 0 件)
15:50:44 人間が 🙋 HITL を除去 = 承認(autopilot-decompose.md が案内している正しい操作)
15:50〜16:27 decompose が再ディスパッチされないphaseForItem が null を返し続ける)
16:27:06 detectStuck が「In Progress/Decomposing で 36 分無進行」→ Blocked

Blocked 化そのものは正しい挙動で、真因は「承認しても再開しない」こと。

原因: applyLabelHealinglabelActions() のガードを迂回している

🧭 tracking分解済み(sub-issue を持つ)Kind=EPIC にだけ付ける規約
.claude/rules/autopilot/state-and-labels.md)。未分解 EPIC に付くと
phaseForItem の早期 return が decompose より手前で作業対象から除外してしまう:

// tools/autopilot/src/phases.js:598
if (hasTrackingLabel(item)) return null;

このデッドロックは #680/#681 で発生し、コミット d397531d8d
fix(autopilot): 未分解 EPIC への 🧭 tracking 付与をやめ decompose デッドロックを解消)で
labelActions()decomposed ガードが入っている:

// tools/autopilot/src/phases.js:1199-1201 (正しい)
const decomposed = Boolean(item && item.subIssues && item.subIssues.total > 0);
if (item && item.kind === 'EPIC' && decomposed && !cur.includes(TRACKING_LABEL)) add.push(TRACKING_LABEL);

ところが applyLabelHealinglabelActions() を呼ばず、同じ判定を自前で再実装しており、
そこに decomposed ガードが無い
:

// tools/autopilot/src/daemon.js:1270 (バグ。現在の develop 295ea33491 に存在)
if (item.kind === 'EPIC' && !labels.includes(TRACKING_LABEL)) add.push(TRACKING_LABEL);

つまり d397531d8d の修正が 2 経路のうち 1 経路にしか当たっていない
これは .claude/rules/autopilot/development.md のレイヤリング不変条件
(判断ロジック = phases.js の純粋関数 / 実行 = daemon)の違反でもある。

発現条件

「Kind=EPIC」かつ「sub-issue 未作成」の状態で label healing の tick を跨ぐと踏む。
decompose が分解案 HITL で停止している間はまさにこの状態なので、
分解案の承認フローを使う EPIC は必ず踏む#1129 が該当)。
既に sub-issue を持つ EPIC(#680/#681/#682)は 🧭 tracking が正当なので影響しない。

やること

  1. applyLabelHealingtools/autopilot/src/daemon.js:1262-1276 付近)の自前ラベル判定を廃止し、
    labelActions()phases.js)に一本化する。labelActions🙋 HITL の付け外しも
    返すため、healing の文脈でどう扱うかを確認すること(steady-state では人間が外した 🙋 を
    再付与しない hitlLabelAction の分岐がある。healing が 🙋 を勝手に再付与しないこと
    必ず担保する — ここを踏み外すと今度は HITL 側で固着する)。
    一本化が難しい場合でも、最低限 decomposed ガードを daemon 側にも入れ、
    判定の二重定義を残さない方針で書くこと。
  2. 同種の「phases.js の純粋関数を迂回して daemon が判定を再実装している」箇所が他に無いか
    監査する(.claude/rules の「パターンは全インスタンスを確認する」に従い、報告された 1 箇所で
    終わりにしない)。見つかれば本 Issue で直すか、フォローアップ Issue に切り出す。
  3. 回帰テストを追加する(tools/autopilot/test/node --test):
    • sub-issue 0 件の Kind=EPIC に 🧭 tracking付けない
    • sub-issue あり Kind=EPIC には付ける
    • healing が人間の外した 🙋 HITL を再付与しない
    • 「未分解 EPIC + In Progress/Decomposing + HITL 解除」で phaseForItemdecompose を返す
      (デッドロックの再発を状態機械側からも検知できるようにする)

補足

  • #1129 自体は運用者が手動復旧済み(🧭 tracking 除去 + Status を In Progress へ戻す +
    🙋 HITL 除去 + POST /inject?issue=1129&phase=decompose)。本 Issue はコード側の恒久対応。
  • 修正は daemon の再起動で反映される(daemon は起動時スナップショットを読む)。

DoD

  • 未分解 EPIC に 🧭 tracking が付かない(daemon 経路でも)
  • ラベル判定の二重定義が解消されている(labelActions() が唯一の真実)
  • healing が人間の外した 🙋 HITL を再付与しない
  • 上記 4 ケースの回帰テストが node --test で green
  • tools/autopilot/ に新しい依存を足していない(標準モジュールのみ)
  • 必要なら .claude/rules/autopilot/state-and-labels.md / development.md の記述を実装に合わせて更新

Metadata

Metadata

Assignees

Labels

🙋 HITL人間の対応待ち(Project HITL=Yes の投影)🤖 autopilotautopilot が処理対象にした Issue/PR(daemon が付与)

Type

No type

Projects

Status
Review

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions