@@ -263,15 +263,43 @@ Merge Request もIssueと同様に `assignees` または `assignee` フィール
263263
264264### 5.2 停止時のコメント
265265
266- タスク停止時に以下のコメントをissue/MR/PRに投稿します:
266+ タスク停止時に以下のコメントをissue/MR/PRに投稿します。
267+ 処理モードに応じて異なるテンプレートを使用します。
267268
269+ ** Planningモードの場合:**
268270``` markdown
269271## ⛔ タスク停止
270272
271273コーディングエージェントのアサインが解除されたため、タスクを停止しました。
272274
273275** 停止時刻:** {ISO 8601形式のタイムスタンプ}
274276** 処理状況:** {実行済みアクション数}/{全アクション数} 完了
277+ ** フェーズ:** {現在のフェーズ(planning/execution/reflection/revision)}
278+
279+ タスクを再開する場合は、コーディングエージェントを再度アサインし、
280+ ` coding agent ` ラベルを付与してください。
281+ ```
282+
283+ ** Context Storageモード(Planning無効)の場合:**
284+ ``` markdown
285+ ## ⛔ タスク停止
286+
287+ コーディングエージェントのアサインが解除されたため、タスクを停止しました。
288+
289+ ** 停止時刻:** {ISO 8601形式のタイムスタンプ}
290+ ** LLM対話回数:** {実行済みのLLM対話回数}
291+
292+ タスクを再開する場合は、コーディングエージェントを再度アサインし、
293+ ` coding agent ` ラベルを付与してください。
294+ ```
295+
296+ ** レガシーモードの場合:**
297+ ``` markdown
298+ ## ⛔ タスク停止
299+
300+ コーディングエージェントのアサインが解除されたため、タスクを停止しました。
301+
302+ ** 停止時刻:** {ISO 8601形式のタイムスタンプ}
275303
276304タスクを再開する場合は、コーディングエージェントを再度アサインし、
277305` coding agent ` ラベルを付与してください。
@@ -345,41 +373,68 @@ TaskStopManager
345373def get_assignees(self) -> list[str]:
346374 """タスクにアサインされているユーザー名のリストを取得する.
347375
376+ この処理は外部API(GitHub/GitLab)を呼び出すため、
377+ ネットワークエラーやAPIエラーが発生する可能性があります。
378+
348379 Returns:
349380 アサインされているユーザー名のリスト
381+
382+ Raises:
383+ Exception: API呼び出しに失敗した場合
384+
385+ Note:
386+ 呼び出し元でエラーハンドリングを行い、
387+ エラー時はタスクを停止せずに処理を継続することを推奨
350388
351389 """
352390```
353391
354392** GitHub Issue 実装例:**
355393``` python
356394def get_assignees (self ) -> list[str ]:
357- """ Issueにアサインされているユーザー名のリストを取得する."""
358- # 最新の情報を取得
359- issue = self .mcp_client.call_tool(
360- " get_issue" ,
361- {" owner" : self .issue[" owner" ], " repo" : self .issue[" repo" ], " issue_number" : self .issue[" number" ]},
362- )
363- return [assignee.get(" login" , " " ) for assignee in issue.get(" assignees" , [])]
395+ """ Issueにアサインされているユーザー名のリストを取得する.
396+
397+ Raises:
398+ Exception: API呼び出しに失敗した場合
399+ """
400+ try :
401+ # 最新の情報を取得
402+ issue = self .mcp_client.call_tool(
403+ " get_issue" ,
404+ {" owner" : self .issue[" owner" ], " repo" : self .issue[" repo" ], " issue_number" : self .issue[" number" ]},
405+ )
406+ return [assignee.get(" login" , " " ) for assignee in issue.get(" assignees" , [])]
407+ except Exception as e:
408+ self .logger.exception(" アサイン情報の取得に失敗: %s " , e)
409+ raise # 呼び出し元でエラーハンドリングを行う
364410```
365411
366412** GitLab Issue 実装例:**
367413``` python
368414def get_assignees (self ) -> list[str ]:
369- """ Issueにアサインされているユーザー名のリストを取得する."""
370- # 最新の情報を取得
371- issue = self .mcp_client.call_tool(
372- " get_issue" ,
373- {" project_id" : str (self .project_id), " issue_iid" : self .issue_iid},
374- )
375- assignees = []
376- # assignees配列があれば使用
377- if issue.get(" assignees" ):
378- assignees = [a.get(" username" , " " ) for a in issue.get(" assignees" , [])]
379- # assignee単体フィールドがあれば追加
380- elif issue.get(" assignee" ):
381- assignees = [issue[" assignee" ].get(" username" , " " )]
382- return assignees
415+ """ Issueにアサインされているユーザー名のリストを取得する.
416+
417+ Raises:
418+ Exception: API呼び出しに失敗した場合
419+ """
420+ try :
421+ # 最新の情報を取得
422+ issue = self .mcp_client.call_tool(
423+ " get_issue" ,
424+ {" project_id" : str (self .project_id), " issue_iid" : self .issue_iid},
425+ )
426+ assignees = []
427+ # assignees配列が存在し、かつ空でない場合
428+ assignees_list = issue.get(" assignees" , [])
429+ if assignees_list and len (assignees_list) > 0 :
430+ assignees = [a.get(" username" , " " ) for a in assignees_list]
431+ # assignees配列がない、または空の場合、assignee単体フィールドを確認
432+ elif issue.get(" assignee" ):
433+ assignees = [issue[" assignee" ].get(" username" , " " )]
434+ return assignees
435+ except Exception as e:
436+ self .logger.exception(" アサイン情報の取得に失敗: %s " , e)
437+ raise # 呼び出し元でエラーハンドリングを行う
383438```
384439
385440### 6.3 TaskHandler への組み込み
@@ -396,22 +451,82 @@ def _handle_with_context_storage(self, task: Task, task_config: dict[str, Any])
396451
397452 # ... 既存の初期化処理 ...
398453
454+ # Check interval counter
455+ check_counter = 0
456+ check_interval = task_config.get(" task_stop" , {}).get(" check_interval" , 1 )
457+
399458 while count < max_count:
400- # Check for pause signal (既存)
401- if pause_manager.check_pause_signal():
402- # ... 一時停止処理 ...
403- return
459+ check_counter += 1
404460
405- # Check assignee status (新規追加)
406- if stop_manager.enabled and not stop_manager.check_assignee_status(task):
407- self .logger.info(" アサイン解除を検出、タスクを停止します" )
408- stop_manager.stop_task(task, task.uuid, " アサインが解除されました" )
409- return # finish()を呼ばずに終了
461+ # 優先順位の取得(デフォルトは pause_first)
462+ priority = task_config.get(" task_stop" , {}).get(" priority" , " pause_first" )
463+
464+ if priority == " pause_first" :
465+ # Check for pause signal first (既存)
466+ if pause_manager.check_pause_signal():
467+ # ... 一時停止処理 ...
468+ return
469+
470+ # Then check assignee status (新規追加)
471+ # check_interval に基づいてチェック頻度を制御
472+ if check_interval > 0 and check_counter % check_interval == 0 :
473+ if stop_manager.enabled and stop_manager.should_check_now():
474+ if not stop_manager.check_assignee_status(task):
475+ self .logger.info(" アサイン解除を検出、タスクを停止します" )
476+ stop_manager.stop_task(task, task.uuid, " アサインが解除されました" )
477+ return # finish()を呼ばずに終了
478+ else : # stop_first
479+ # Check assignee status first
480+ if check_interval > 0 and check_counter % check_interval == 0 :
481+ if stop_manager.enabled and stop_manager.should_check_now():
482+ if not stop_manager.check_assignee_status(task):
483+ self .logger.info(" アサイン解除を検出、タスクを停止します" )
484+ stop_manager.stop_task(task, task.uuid, " アサインが解除されました" )
485+ return
486+
487+ # Then check for pause signal
488+ if pause_manager.check_pause_signal():
489+ # ... 一時停止処理 ...
490+ return
410491
411492 # ... 既存の処理ループ ...
412493```
413494
414- ### 6.4 PlanningCoordinator への組み込み
495+ ### 6.4 TaskStopManager の should_check_now メソッド
496+
497+ ` min_check_interval_seconds ` 設定に基づき、APIレート制限を考慮したチェックを実装します。
498+
499+ ``` python
500+ class TaskStopManager :
501+ def __init__ (self , config : dict[str , Any]) -> None :
502+ # ... 既存の初期化処理 ...
503+ task_stop_config = config.get(" task_stop" , {})
504+ self .min_check_interval_seconds = task_stop_config.get(" min_check_interval_seconds" , 30 )
505+ self ._last_check_time: float | None = None
506+
507+ def should_check_now (self ) -> bool :
508+ """ min_check_interval_seconds を考慮して、チェックを実行すべきかを判定.
509+
510+ Returns:
511+ True: チェックを実行すべき
512+ False: 前回のチェックから十分な時間が経過していない
513+ """
514+ import time
515+ current_time = time.time()
516+
517+ if self ._last_check_time is None :
518+ self ._last_check_time = current_time
519+ return True
520+
521+ elapsed = current_time - self ._last_check_time
522+ if elapsed >= self .min_check_interval_seconds:
523+ self ._last_check_time = current_time
524+ return True
525+
526+ return False
527+ ```
528+
529+ ### 6.5 PlanningCoordinator への組み込み
415530
416531` PlanningCoordinator ` クラスにも同様にアサインチェックを追加します。
417532
@@ -640,17 +755,30 @@ API呼び出しがタイムアウトした場合:
640755** シナリオS9: アサイン解除と一時停止シグナルの同時発生**
6417561 . タスク処理を開始
6427572 . 同じタイミングでアサイン解除と ` pause_signal ` ファイル作成
643- 3 . 一時停止が優先されることを確認(または設定で優先順位を変更可能)
758+ 3 . 設定に応じた優先順位で処理されることを確認
759+
760+ ** 優先順位の実装:**
761+ セクション6.3「TaskHandler への組み込み」で説明したように、` priority ` 設定に基づいて処理順序を制御します。
644762
645763** 設定例:**
646764``` yaml
647765task_stop :
648766 # 一時停止シグナルとの優先順位
649- # "pause_first": 一時停止を優先
767+ # "pause_first": 一時停止を優先(デフォルト)
768+ # - 一時停止シグナルが先にチェックされ、状態が保存される
769+ # - 後で再開可能
650770 # "stop_first": 停止を優先
771+ # - アサイン解除が先にチェックされ、タスクが終了する
772+ # - 再開不可(新規タスクとして再実行が必要)
651773 priority : " pause_first"
652774` ` `
653775
776+ **シナリオS10: 優先順位 stop_first での同時発生**
777+ 1. ` priority: "stop_first"` を設定
778+ 2. タスク処理を開始
779+ 3. 同じタイミングでアサイン解除と `pause_signal` ファイル作成
780+ 4. 停止が優先されることを確認(一時停止ではなく終了)
781+
654782# # 10. 運用ガイドライン
655783
656784# ## 10.1 タスク停止の実行方法
0 commit comments