症状
未分解の 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 化そのものは正しい挙動で、真因は「承認しても再開しない」こと。
原因: applyLabelHealing が labelActions() のガードを迂回している
🧭 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);
ところが applyLabelHealing は labelActions() を呼ばず、同じ判定を自前で再実装しており、
そこに 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 が正当なので影響しない。
やること
applyLabelHealing(tools/autopilot/src/daemon.js:1262-1276 付近)の自前ラベル判定を廃止し、
labelActions()(phases.js)に一本化する。labelActions は 🙋 HITL の付け外しも
返すため、healing の文脈でどう扱うかを確認すること(steady-state では人間が外した 🙋 を
再付与しない hitlLabelAction の分岐がある。healing が 🙋 を勝手に再付与しないことを
必ず担保する — ここを踏み外すと今度は HITL 側で固着する)。
一本化が難しい場合でも、最低限 decomposed ガードを daemon 側にも入れ、
判定の二重定義を残さない方針で書くこと。
- 同種の「
phases.js の純粋関数を迂回して daemon が判定を再実装している」箇所が他に無いか
監査する(.claude/rules の「パターンは全インスタンスを確認する」に従い、報告された 1 箇所で
終わりにしない)。見つかれば本 Issue で直すか、フォローアップ Issue に切り出す。
- 回帰テストを追加する(
tools/autopilot/test/、node --test):
- sub-issue 0 件の Kind=EPIC に
🧭 tracking を 付けない
- sub-issue あり Kind=EPIC には付ける
- healing が人間の外した
🙋 HITL を再付与しない
- 「未分解 EPIC + In Progress/Decomposing + HITL 解除」で
phaseForItem が decompose を返す
(デッドロックの再発を状態機械側からも検知できるようにする)
補足
#1129 自体は運用者が手動復旧済み(🧭 tracking 除去 + Status を In Progress へ戻す +
🙋 HITL 除去 + POST /inject?issue=1129&phase=decompose)。本 Issue はコード側の恒久対応。
- 修正は daemon の再起動で反映される(daemon は起動時スナップショットを読む)。
DoD
症状
未分解の EPIC(sub-issue 0 件)に
🧭 trackingが付与され、decompose が永久に走らなくなる。#1129 で実際に発生した。経緯:
🙋 HITLで承認待ち(Status=In Progress / AI Status=Decomposing のまま)🧭 trackingを付与(sub-issue は 0 件)🙋 HITLを除去 = 承認(autopilot-decompose.mdが案内している正しい操作)phaseForItemが null を返し続ける)detectStuckが「In Progress/Decomposing で 36 分無進行」→ BlockedBlocked 化そのものは正しい挙動で、真因は「承認しても再開しない」こと。
原因:
applyLabelHealingがlabelActions()のガードを迂回している🧭 trackingは 分解済み(sub-issue を持つ)Kind=EPIC にだけ付ける規約(
.claude/rules/autopilot/state-and-labels.md)。未分解 EPIC に付くとphaseForItemの早期 return が decompose より手前で作業対象から除外してしまう:このデッドロックは #680/#681 で発生し、コミット
d397531d8d(
fix(autopilot): 未分解 EPIC への 🧭 tracking 付与をやめ decompose デッドロックを解消)でlabelActions()にdecomposedガードが入っている:ところが
applyLabelHealingはlabelActions()を呼ばず、同じ判定を自前で再実装しており、そこに
decomposedガードが無い:つまり
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が正当なので影響しない。やること
applyLabelHealing(tools/autopilot/src/daemon.js:1262-1276付近)の自前ラベル判定を廃止し、labelActions()(phases.js)に一本化する。labelActionsは🙋 HITLの付け外しも返すため、healing の文脈でどう扱うかを確認すること(steady-state では人間が外した 🙋 を
再付与しない
hitlLabelActionの分岐がある。healing が 🙋 を勝手に再付与しないことを必ず担保する — ここを踏み外すと今度は HITL 側で固着する)。
一本化が難しい場合でも、最低限
decomposedガードを daemon 側にも入れ、判定の二重定義を残さない方針で書くこと。
phases.jsの純粋関数を迂回して daemon が判定を再実装している」箇所が他に無いか監査する(
.claude/rulesの「パターンは全インスタンスを確認する」に従い、報告された 1 箇所で終わりにしない)。見つかれば本 Issue で直すか、フォローアップ Issue に切り出す。
tools/autopilot/test/、node --test):🧭 trackingを 付けない🙋 HITLを再付与しないphaseForItemがdecomposeを返す(デッドロックの再発を状態機械側からも検知できるようにする)
補足
#1129自体は運用者が手動復旧済み(🧭 tracking除去 + Status を In Progress へ戻す +🙋 HITL除去 +POST /inject?issue=1129&phase=decompose)。本 Issue はコード側の恒久対応。DoD
🧭 trackingが付かない(daemon 経路でも)labelActions()が唯一の真実)🙋 HITLを再付与しないnode --testで greentools/autopilot/に新しい依存を足していない(標準モジュールのみ).claude/rules/autopilot/state-and-labels.md/development.mdの記述を実装に合わせて更新