github-issue-tower-defence-management 1.187.3 → 1.188.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +3 -0
  3. package/bin/adapter/entry-points/cli/index.js +20 -12
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
  6. package/bin/adapter/entry-points/console/ui-dist/assets/index-CA6c7Pfz.js +86 -0
  7. package/bin/adapter/entry-points/console/ui-dist/assets/index-D1xvwesF.css +2 -0
  8. package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
  9. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +1 -0
  10. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  11. package/bin/domain/usecases/HandleScheduledEventUseCase.js +1 -0
  12. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  13. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +3 -4
  14. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
  15. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +3 -3
  16. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
  17. package/bin/domain/usecases/TriagerApprovalDispatchUseCase.js +13 -1
  18. package/bin/domain/usecases/TriagerApprovalDispatchUseCase.js.map +1 -1
  19. package/bin/domain/usecases/isHumanComment.js +21 -0
  20. package/bin/domain/usecases/isHumanComment.js.map +1 -0
  21. package/bin/domain/usecases/nextStepAgentDispatchRepeatedMessage.js +5 -0
  22. package/bin/domain/usecases/nextStepAgentDispatchRepeatedMessage.js.map +1 -0
  23. package/bin/domain/usecases/resolveNextStepAgentDispatchRepetition.js +62 -12
  24. package/bin/domain/usecases/resolveNextStepAgentDispatchRepetition.js.map +1 -1
  25. package/package.json +1 -1
  26. package/src/adapter/entry-points/cli/index.test.ts +2 -0
  27. package/src/adapter/entry-points/cli/index.ts +39 -16
  28. package/src/adapter/entry-points/cli/projectConfig.ts +1 -0
  29. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.stories.tsx +39 -1
  30. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.test.tsx +61 -0
  31. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.tsx +92 -2
  32. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +2 -0
  33. package/src/adapter/entry-points/console/ui/src/index.css +50 -0
  34. package/src/adapter/entry-points/console/ui-dist/assets/index-CA6c7Pfz.js +86 -0
  35. package/src/adapter/entry-points/console/ui-dist/assets/index-D1xvwesF.css +2 -0
  36. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  37. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +2 -0
  38. package/src/domain/usecases/HandleScheduledEventUseCase.ts +2 -0
  39. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +98 -0
  40. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +9 -5
  41. package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +9 -4
  42. package/src/domain/usecases/TriagerApprovalDispatchUseCase.test.ts +88 -0
  43. package/src/domain/usecases/TriagerApprovalDispatchUseCase.ts +17 -2
  44. package/src/domain/usecases/isHumanComment.test.ts +90 -0
  45. package/src/domain/usecases/isHumanComment.ts +23 -0
  46. package/src/domain/usecases/nextStepAgentDispatchRepeatedMessage.ts +2 -0
  47. package/src/domain/usecases/resolveNextStepAgentDispatchRepetition.test.ts +312 -72
  48. package/src/domain/usecases/resolveNextStepAgentDispatchRepetition.ts +103 -19
  49. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  50. package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
  51. package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
  52. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  53. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +1 -0
  54. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  55. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +1 -0
  56. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
  57. package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts +1 -0
  58. package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts.map +1 -1
  59. package/types/domain/usecases/TriagerApprovalDispatchUseCase.d.ts.map +1 -1
  60. package/types/domain/usecases/isHumanComment.d.ts +5 -0
  61. package/types/domain/usecases/isHumanComment.d.ts.map +1 -0
  62. package/types/domain/usecases/nextStepAgentDispatchRepeatedMessage.d.ts +2 -0
  63. package/types/domain/usecases/nextStepAgentDispatchRepeatedMessage.d.ts.map +1 -0
  64. package/types/domain/usecases/resolveNextStepAgentDispatchRepetition.d.ts +5 -2
  65. package/types/domain/usecases/resolveNextStepAgentDispatchRepetition.d.ts.map +1 -1
  66. package/bin/adapter/entry-points/console/ui-dist/assets/index-DfKDTGzS.js +0 -86
  67. package/bin/adapter/entry-points/console/ui-dist/assets/index-yZy1TUOo.css +0 -2
  68. package/src/adapter/entry-points/console/ui-dist/assets/index-DfKDTGzS.js +0 -86
  69. package/src/adapter/entry-points/console/ui-dist/assets/index-yZy1TUOo.css +0 -2
@@ -0,0 +1,2 @@
1
+ /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-font-weight:initial;--tw-outline-style:solid;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-medium:500;--font-weight-semibold:600;--radius-md:.375rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-input:#e5e5e5;--color-ring:#0a0a0a;--color-background:#fff;--color-foreground:#0a0a0a;--color-primary:#171717;--color-primary-foreground:#fafafa;--color-secondary:#f5f5f5;--color-secondary-foreground:#171717;--color-accent:#f5f5f5;--color-accent-foreground:#171717}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.visible{visibility:visible}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.block{display:block}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.grow{flex-grow:1}.items-center{align-items:center}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing) * 2)}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-input{border-color:var(--color-input)}.border-transparent{border-color:#0000}.bg-background{background-color:var(--color-background)}.bg-primary{background-color:var(--color-primary)}.bg-secondary{background-color:var(--color-secondary)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-2{padding-block:calc(var(--spacing) * 2)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.whitespace-nowrap{white-space:nowrap}.text-foreground{color:var(--color-foreground)}.text-primary-foreground{color:var(--color-primary-foreground)}.text-secondary-foreground{color:var(--color-secondary-foreground)}.lowercase{text-transform:lowercase}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.hover\:bg-accent:hover{background-color:var(--color-accent)}.hover\:bg-primary\/90:hover{background-color:#171717e6}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab, var(--color-primary) 90%, transparent)}}.hover\:bg-secondary\/80:hover{background-color:#f5f5f5cc}@supports (color:color-mix(in lab, red, red)){.hover\:bg-secondary\/80:hover{background-color:color-mix(in oklab, var(--color-secondary) 80%, transparent)}}.hover\:text-accent-foreground:hover{color:var(--color-accent-foreground)}}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color:var(--color-ring)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-50:disabled{opacity:.5}}html{font-size:clamp(15px,2.1vw,26px)}body{color:#e6edf3;background-color:#0d1117;margin:0;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif}.console-app{flex-direction:column;width:100%;min-height:100dvh;display:flex}.console-tabbar{-webkit-overflow-scrolling:touch;background:#161b22;border-bottom:2px solid #30363d;flex-wrap:nowrap;align-items:stretch;gap:0;min-height:42px;padding:0 8px;display:flex;overflow-x:auto}.console-tab{color:#8b949e;white-space:nowrap;cursor:pointer;background:0 0;border:none;border-bottom:2px solid #0000;align-items:center;gap:6px;margin-bottom:-2px;padding:10px 14px;font-size:.8125rem;font-weight:500;line-height:1.2;text-decoration:none;display:inline-flex}.console-tab:hover{color:#e6edf3}.console-tab[data-active=true]{color:#e6edf3;border-bottom-color:#2f81f7;font-weight:700}.console-tab-badge{text-align:center;color:#e6edf3;background:#484f58;border-radius:20px;min-width:20px;padding:1px 7px;font-size:.6875rem;font-weight:700;line-height:1.5}.console-tab-badge[data-zero=true]{color:#8b949e;background:#30363d}.console-tab-pjname{align-self:center;margin-left:auto;padding:0 8px}.console-tab-pjname-button{color:#8b949e;cursor:pointer;background:0 0;border:none;border-radius:4px;align-items:center;gap:3px;padding:2px 4px;font-size:.7188rem;line-height:1;display:flex}.console-tab-pjname-button:hover{color:#e6edf3;background:#21262d}.console-tab-pjname-arrow{font-size:.625rem}.console-tab-pjname-dropdown{z-index:100;background:#161b22;border:1px solid #30363d;border-radius:6px;min-width:120px;margin:0;padding:4px 0;list-style:none;box-shadow:0 8px 24px #01040980}.console-tab-pjname-option{color:#e6edf3;cursor:pointer;text-align:left;background:0 0;border:none;width:100%;padding:4px 12px;font-size:.7188rem;display:block}.console-tab-pjname-option:hover,.console-tab-pjname-option[data-active=true]{background:#21262d}.console-tab-geninfo{color:#8b949e;align-self:center;padding:0 4px;font-size:.6875rem}.console-tab-settings{align-self:center;padding:0 4px}.console-timer-settings-button{color:#8b949e;cursor:pointer;background:0 0;border:none;border-radius:4px;padding:4px 8px;font-size:1rem;line-height:1}.console-timer-settings-button:hover{color:#e6edf3;background:#21262d}.console-timer-settings-dialog{z-index:100;justify-content:flex-end;align-items:flex-start;padding:48px 16px 0;display:flex;position:fixed;inset:0}.console-timer-settings-backdrop{cursor:default;z-index:-1;background:#01040999;border:none;position:fixed;inset:0}.console-timer-settings-dialog-inner{background:#161b22;border:1px solid #30363d;border-radius:8px;min-width:280px;max-width:400px;max-height:80vh;padding:20px;overflow-y:auto}.console-timer-settings-title{color:#e6edf3;margin:0 0 16px;font-size:.9375rem;font-weight:600}.console-timer-settings-row{justify-content:space-between;align-items:center;margin-bottom:16px;display:flex}.console-timer-settings-label{color:#c9d1d9;font-size:.875rem}.console-timer-settings-loading{color:#8b949e;padding:8px 0;font-size:.875rem}.console-timer-settings-project-list{margin:0;padding:0;list-style:none}.console-timer-settings-project-row{border-bottom:1px solid #21262d;align-items:center;gap:8px;padding:6px 0;display:flex}.console-timer-settings-project-row:last-child{border-bottom:none}.console-timer-settings-pjcode{color:#c9d1d9;flex:1;font-family:ui-monospace,SFMono-Regular,monospace;font-size:.8125rem}.console-timer-settings-minutes-input{color:#e6edf3;text-align:right;background:#0d1117;border:1px solid #30363d;border-radius:4px;width:56px;padding:3px 6px;font-size:.8125rem}.console-timer-settings-minutes-label{color:#8b949e;width:24px;font-size:.75rem}.console-timer-settings-actions{justify-content:flex-end;margin-top:16px;display:flex}.console-timer-settings-save{color:#fff;cursor:pointer;background:#238636;border:1px solid #f0f6fc1a;border-radius:6px;padding:6px 14px;font-size:.8125rem;font-weight:500}.console-timer-settings-save:hover{background:#2ea043}.console-list{margin:0;padding:12px 18px 18px;list-style:none}.console-list-group{list-style:none}.console-item-row .console-item-icon{flex:none;margin-top:3px}.console-group-header{background:#0b0f14;border-bottom:1px solid #21262d;justify-content:space-between;align-items:center;padding:6px 12px;display:flex}.console-storytag{align-items:center;gap:8px;font-size:.8125rem;font-weight:700;display:inline-flex}.console-story-dot{border-radius:999px;width:10px;height:10px;display:inline-block}.console-group-count{color:#8b949e;font-size:.75rem}.console-story-color-palette{flex-wrap:wrap;gap:4px;padding:6px 0;display:flex}.console-story-color-swatch{cursor:pointer;color:#e6edf3;border:2px solid #0000;border-radius:6px;justify-content:center;align-items:center;width:28px;height:28px;padding:0;font-size:.625rem;display:inline-flex}.console-story-color-swatch:hover{border-color:#e6edf3}.console-story-color-swatch:disabled{opacity:.4;cursor:not-allowed}.console-story-color-swatch-label{font-size:.5rem;line-height:1}.console-item-row{color:#e6edf3;text-align:left;cursor:pointer;background:#161b22;border:1px solid #30363d;border-radius:8px;align-items:flex-start;gap:14px;width:100%;margin-bottom:10px;padding:12px 16px;display:flex}.console-item-row:hover{background:#1a2029;border-color:#484f58}.console-item-row[data-active=true]{background:#1a2029;border-color:#4493f8}.console-item-meta{flex:1;min-width:0}.console-item-title{font-size:.9063rem;font-weight:600;display:block}.console-item-sub{color:#8b949e;margin-top:3px;font-size:.7813rem;display:block}.console-item-pill{color:#8b949e;border:1px solid #30363d;border-radius:20px;margin-right:6px;padding:1px 8px;font-size:.6875rem;display:inline-block}.console-item-createdat{color:#8b949e;cursor:help}.console-item-fields{color:#adbac7;flex-wrap:wrap;gap:4px 10px;margin-top:4px;font-size:.7188rem;display:flex}.console-item-field{word-break:break-all;align-items:baseline;gap:4px;min-width:0;display:inline-flex}.console-item-field-label{color:#6e7681;text-transform:uppercase;letter-spacing:.03em;font-size:.625rem}.console-list-message{color:#8b949e;padding:16px;font-size:.875rem}.console-list-empty{text-align:center;color:#8b949e;padding:40px;font-size:.875rem}.console-list-error,.console-detail-fetch-error{color:#f85149}.console-detail-fetch-error-line{margin:0}.console-comment-notloaded,.console-files-notloaded,.console-commits-notloaded,.console-detail-body-notloaded{color:#8b949e}.console-detail{flex-direction:column;flex:1 0 auto;gap:12px;padding:16px 16px 0;display:flex}.console-detail-topline{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.console-detail-title{align-items:center;gap:8px;margin:0;font-size:1.25rem;display:flex}.console-detail-title-text{flex:1}.console-detail-number{color:#8b949e;font-weight:400}.console-detail-closed-label{color:#a371f7;font-size:.8125rem}.console-detail-subbar{align-items:center;gap:12px;font-size:.8125rem;display:flex}.console-detail-link{color:#4493f8}.console-detail-repo{color:#8b949e;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.console-copy-url-button{color:#c9d1d9;border:1px solid #30363d;border-radius:6px;height:24px;padding:0 8px;font-size:.75rem}.console-copy-url-button:hover{color:#e6edf3;background-color:#21262d}.console-detail-pill,.console-label-chip,.console-detail-status-chip{border:1px solid #30363d;border-radius:999px;padding:2px 8px;font-size:.75rem;display:inline-block}.console-detail-agent-chip{color:#8b949e;border:1px solid #30363d;border-radius:999px;padding:2px 8px;font-size:.75rem;font-weight:400;display:inline-block}.console-detail-pr-status{flex-wrap:wrap;align-items:center;gap:6px;display:inline-flex}.console-related-pr-group{align-items:center;gap:6px;display:inline-flex}.console-related-pr-label{color:#8b949e;font-size:.75rem}.console-detail-ci-missing{opacity:.85;font-weight:400}.console-detail-mergeable-chip{font-weight:600}.console-detail-mergeable-chip-unknown{opacity:.7;font-weight:400}.console-detail-labels{flex-wrap:wrap;gap:6px;display:flex}.console-detail-createdat{color:#8b949e;font-size:.75rem}.console-panel{border:1px solid #30363d;border-radius:8px;overflow:hidden}.console-panel-header{background:#161b22;justify-content:space-between;align-items:center;display:flex}.console-panel-toggle{color:#e6edf3;text-align:left;cursor:pointer;background:0 0;border:none;flex:1;align-items:center;gap:8px;padding:6px 12px;font-size:.875rem;font-weight:600;display:flex}.console-panel-toggle:hover{background:#1a2029}.console-panel-action{padding:6px 12px}.console-panel-body{padding:12px}.console-markdown{word-break:break-word;font-size:.875rem;line-height:1.5}.console-markdown ul{margin:.5em 0;padding-left:1.5em;list-style:outside}.console-markdown ol{margin:.5em 0;padding-left:1.5em;list-style:decimal}.console-markdown li{margin:.25em 0}.console-markdown table{border-collapse:collapse;margin:.5em 0}.console-markdown th,.console-markdown td{border:1px solid #30363d;padding:4px 8px}.console-markdown th{background:#161b22}.console-markdown h1,.console-markdown h2,.console-markdown h3,.console-markdown h4,.console-markdown h5,.console-markdown h6{margin:1em 0 .5em;font-weight:700;line-height:1.25}.console-markdown h1{font-size:1.6em}.console-markdown h2{font-size:1.4em}.console-markdown h3{font-size:1.2em}.console-markdown h4{font-size:1.05em}.console-markdown h5{font-size:.95em}.console-markdown h6{color:#8b949e;font-size:.9em}.console-markdown pre{background:#161b22;border:1px solid #30363d;border-radius:6px;margin:.5em 0;padding:12px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.8125rem;line-height:1.45;overflow-x:auto}.console-markdown code{background:#6e768166;border-radius:4px;padding:.15em .4em;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.9em}.console-markdown pre code{font-size:inherit;background:0 0;border-radius:0;padding:0}.console-markdown-code-block{flex-direction:column;margin:.5em 0;display:flex}.console-markdown-code-block pre{margin:0}.console-markdown-code-copy-host{justify-content:flex-end;margin-bottom:4px;display:flex}.console-copy-code-button{color:#c9d1d9;background-color:#161b22;border:1px solid #30363d;border-radius:6px;height:24px;padding:0 8px;font-size:.75rem}.console-copy-code-button:hover{color:#e6edf3;background-color:#21262d}.console-markdown blockquote{color:#8b949e;border-left:3px solid #30363d;margin:.5em 0;padding:0 1em}.console-markdown a{color:#4493f8;text-decoration:none}.console-markdown a:hover{text-decoration:underline}.console-markdown-reference-host{display:inline}.console-markdown-reference{vertical-align:baseline;align-items:center;gap:.25em;display:inline-flex}.console-markdown-reference .console-item-icon{flex-shrink:0}.console-markdown-reference-title{text-decoration:none}.console-markdown-reference:hover .console-markdown-reference-title{text-decoration:underline}.console-markdown hr{border:none;border-top:1px solid #30363d;margin:1em 0}.console-mermaid-error{color:#f85149;font-size:.8125rem}.console-mermaid-enlarge{color:#e6edf3;background:#21262d;border:1px solid #30363d;border-radius:6px;width:100%;margin-top:6px;padding:10px 12px;font-size:.8125rem;display:block}.console-mermaid-modal{z-index:1000;background:#0d1117;flex-direction:column;display:flex;position:fixed;inset:0}.console-mermaid-modal-bar{border-bottom:1px solid #30363d;align-items:center;gap:8px;padding:8px 12px;display:flex}.console-mermaid-modal-button,.console-mermaid-modal-close{color:#e6edf3;background:#21262d;border:1px solid #30363d;border-radius:6px;min-width:44px;min-height:44px;font-size:1.125rem}.console-mermaid-modal-button:disabled{opacity:.4}.console-mermaid-modal-close{margin-left:auto}.console-mermaid-modal-scale{text-align:center;color:#8b949e;min-width:4.5rem;font-size:.875rem}.console-mermaid-modal-viewport{-webkit-overflow-scrolling:touch;flex:1;overflow:auto}.console-mermaid-modal-canvas{min-width:100%}.console-comment{background:#161b22;border:1px solid #30363d;border-radius:8px;margin-bottom:10px;padding:12px 16px}.console-comment-header{color:#8b949e;gap:8px;font-size:.75rem;display:flex}.console-comment-show-all{color:#e6edf3;cursor:pointer;background:#21262d;border:1px solid #30363d;border-radius:6px;margin-bottom:10px;padding:6px 12px;font-size:.75rem;font-weight:600}.console-comment-show-all:hover{background-color:#262c34;border-color:#484f58}.console-comment-author{color:#e6edf3;font-weight:600}.console-files,.console-commits{margin:0;padding:0;list-style:none}.console-commit{align-items:center;gap:8px;padding:4px 0;font-size:.8125rem;display:flex}.console-file{flex-direction:column;font-size:.8125rem;display:flex}.console-file-tree,.console-file-tree-children{margin:0;padding:0;list-style:none}.console-file-tree-dir{flex-direction:column;display:flex}.console-file-tree-dir-name{color:#8b949e;align-items:center;gap:6px;padding:4px 0;font-weight:600;display:flex}.console-file-tree-dir-icon{flex:none}.console-file-badge{text-align:center;border:1px solid;border-radius:4px;width:18px;font-size:.6875rem}.console-file-path,.console-commit-message{flex:1;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.console-file-add,.console-pr-add{color:#3fb950}.console-file-del,.console-pr-del{color:#f85149}.console-commit-sha{color:#8b949e;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.console-detail-dock{z-index:100;background:#161b22;border-top:2px solid #30363d;margin-top:auto;position:sticky;bottom:0}.console-detail-dock-composer{max-height:50vh;padding:8px 16px 0;overflow-y:auto}.console-actionbar{padding:10px 16px;padding-bottom:calc(10px + env(safe-area-inset-bottom));background:#161b22}.console-operation-bar{flex-direction:column;gap:8px;width:100%;display:flex}.console-op-group{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:8px;display:flex}.console-op-group-review{gap:10px}.console-op-group-stories{max-height:33vh;overflow-y:auto}.console-op-button{color:#e6edf3;white-space:nowrap;cursor:pointer;background:#21262d;border:1px solid #30363d;border-radius:6px;padding:9px 16px;font-size:.8125rem;font-weight:600}.console-op-button:hover{border-color:#484f58}.console-op-button:disabled{opacity:.4;cursor:not-allowed}.console-op-requirement{text-align:right;color:#8b949e;width:100%;margin:0;font-size:.75rem}.console-op-button-approve{color:#fff;background:#238636;border-color:#2ea043}.console-op-button-reject{color:#ffd166;background:#7d5000;border-color:#a06800}.console-op-button-wrong{color:#f85149;background:#3a1518;border-color:#f85149}.console-op-button-unneeded{color:#aab0b8;background:#2a2d31;border-color:#6e7681}.console-op-button-snooze{color:#79c0ff;background:#1c2b4a;border-color:#4493f8}.console-pr-section{border:1px solid #30363d;border-radius:8px;flex-direction:column;gap:10px;padding:12px;display:flex}.console-pr-statbar{color:#8b949e;gap:12px;font-size:.75rem;display:flex}.console-pr-repo{color:#8b949e;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.console-detail-screen{flex-direction:column;flex:1 0 auto;display:flex}.console-panel-open-link{color:#4493f8;font-size:.8125rem;font-weight:400}.console-composer{margin-top:4px}.console-composer-toggle{color:#8b949e;cursor:pointer;background:0 0;border:none;margin-left:auto;padding:2px 0;font-size:.7813rem;display:block}.console-composer-toggle:hover{color:#e6edf3}.console-composer-form{margin-top:8px}.console-composer-input{box-sizing:border-box;color:#e6edf3;width:100%;font:inherit;resize:vertical;background:#21262d;border:1px solid #30363d;border-radius:6px;padding:8px;font-size:.875rem}.console-composer-attach{align-items:center;gap:8px;padding:0 10px;display:flex}.console-composer-file-input{display:none}.console-composer-attach-button{color:#c9d1d9;cursor:pointer;background:#21262d;border:1px solid #30363d;border-radius:4px;padding:4px 8px;font-size:.75rem}.console-composer-attach-button:disabled{opacity:.6;cursor:default}.console-composer-row{justify-content:flex-end;align-items:center;gap:8px;margin-top:6px;display:flex}.console-composer-submit{color:#fff;cursor:pointer;background:#238636;border:1px solid #2ea043;border-radius:6px;padding:7px 16px;font-size:.8125rem;font-weight:600}.console-composer-submit:disabled{opacity:.6;cursor:default}.console-composer-status{color:#8b949e;font-size:.75rem}.console-composer-error{color:#f85149}.console-undo-toast{z-index:9998;white-space:nowrap;border-radius:10px;align-items:center;gap:12px;max-width:92vw;padding:12px 18px;font-size:.8438rem;display:flex;position:fixed;top:12px;left:50%;transform:translate(-50%);box-shadow:0 4px 20px #0009}.console-undo-toast-green{color:#3fb950;background:#0d3320;border:1px solid #2ea043}.console-undo-toast-amber{color:#ffd166;background:#3a2800;border:1px solid #a06800}.console-undo-toast-red{color:#f85149;background:#3a1518;border:1px solid #da3633}.console-undo-toast-gray{color:#aab0b8;background:#2a2d31;border:1px solid #6e7681}.console-undo-toast-blue{color:#79c0ff;background:#0d1f40;border:1px solid #4493f8}.console-undo-toast-error{color:#f85149;background:#3a1518;border:1px solid #f85149}.console-undo-toast-message{text-overflow:ellipsis;flex:1;min-width:0;overflow:hidden}.console-undo-toast-undo{cursor:pointer;color:inherit;background:#ffffff1f;border:none;border-radius:6px;flex:none;padding:4px 12px;font-size:.8125rem;font-weight:700}.console-undo-toast-undo:hover{background:#ffffff38}.console-undo-toast-countdown{opacity:.7;flex:none;font-size:.6875rem}.console-undo-toast-bar{opacity:.5;background:currentColor;border-radius:0 0 10px 10px;height:3px;transition:width .1s linear;position:absolute;bottom:0;left:0}.console-file-row{width:100%;color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;align-items:center;gap:8px;padding:4px 0;font-size:.8125rem;display:flex}.console-file-row:hover{background:#1a2029}.console-file-caret{color:#8b949e;flex:none;width:1em}.console-file-diff{border-collapse:collapse;width:100%;margin:4px 0 8px;font:.8125rem/1.6 ui-monospace,SFMono-Regular,Menlo,monospace}.console-file-diff td{white-space:pre-wrap;word-break:break-word;vertical-align:top;padding:0 10px}.console-diff-ln{text-align:right;color:#6e7681;-webkit-user-select:none;user-select:none;white-space:nowrap;border-right:1px solid #30363d;width:1%;padding:0 8px}.console-file-diff td.console-diff-ln{white-space:nowrap;word-break:normal;overflow-wrap:normal}.console-diff-add td{background:#2ea04326}.console-diff-add .console-diff-code{color:#aff5b4}.console-diff-del td{background:#f8514926}.console-diff-del .console-diff-code{color:#ffdcd7}.console-diff-hunk td{color:#79c0ff;background:#161b22}.console-diff-ctx .console-diff-code{color:#c9d1d9}.console-file-diff-empty{color:#8b949e;padding:8px 14px;font-size:.75rem}.console-file-diff-image{padding:8px 14px}.console-file-diff-image-content{object-fit:contain;object-position:left top;border:1px solid #30363d;border-radius:4px;max-width:100%;max-height:420px}.console-diff-comment-cell{text-align:center;-webkit-user-select:none;user-select:none;width:1%;padding:0 2px}.console-diff-comment-button{color:#6e7681;cursor:pointer;opacity:.6;background:0 0;border:1px solid #30363d;border-radius:4px;width:18px;height:18px;padding:0;font-size:.875rem;line-height:16px}.console-diff-row:hover .console-diff-comment-button,.console-diff-comment-button:focus-visible,.console-diff-comment-button[aria-expanded=true]{opacity:1;color:#fff;background:#1f6feb;border-color:#2f81f7}.console-diff-composer-row td{background:#0d1117;padding:6px 10px}.console-diff-composer{background:#161b22;border:1px solid #30363d;border-radius:6px;flex-direction:column;gap:6px;padding:8px;display:flex}.console-diff-composer-anchor{color:#8b949e;font-size:.6875rem}.console-diff-composer-input{resize:vertical;color:#c9d1d9;width:100%;font:inherit;background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:6px 8px}.console-diff-composer-controls{align-items:center;gap:8px;display:flex}.console-diff-composer-submit{color:#fff;cursor:pointer;background:#238636;border:1px solid #238636;border-radius:6px;padding:4px 12px}.console-diff-composer-submit:disabled{opacity:.6;cursor:default}.console-diff-composer-cancel{color:#c9d1d9;cursor:pointer;background:0 0;border:1px solid #30363d;border-radius:6px;padding:4px 12px}.console-diff-composer-status{color:#8b949e;font-size:.75rem}.console-diff-composer-error{color:#ff7b72}.console-diff-composer-posted{color:#3fb950;margin:0;font-size:.75rem}.console-pr-header{flex-wrap:wrap;align-items:center;gap:10px;padding-top:4px;display:flex}.console-pr-section-title{color:#e6edf3;font-size:.9375rem;font-weight:600}.console-pr-section-state{color:#8b949e;border:1px solid #6e7681;border-radius:999px;padding:1px 8px;font-size:.6875rem}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>TDPM Console</title>
7
- <script type="module" crossorigin src="/assets/index-DfKDTGzS.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-yZy1TUOo.css">
7
+ <script type="module" crossorigin src="/assets/index-CA6c7Pfz.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-D1xvwesF.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -301,6 +301,8 @@ export class HandleScheduledEventUseCaseHandler {
301
301
  input.claudeCodeOauthTokenListJsonPath,
302
302
  thresholdForAutoReject:
303
303
  readmeConfig.thresholdForAutoReject ?? input.thresholdForAutoReject,
304
+ thresholdForDispatchLoop:
305
+ readmeConfig.thresholdForDispatchLoop ?? input.thresholdForDispatchLoop,
304
306
  developerAgentName:
305
307
  readmeConfig.developerAgentName ?? input.developerAgentName ?? null,
306
308
  startPreparation: input.startPreparation
@@ -188,6 +188,7 @@ export class HandleScheduledEventUseCase {
188
188
  labelsAsLlmAgentName?: string[] | null;
189
189
  } | null;
190
190
  thresholdForAutoReject?: number;
191
+ thresholdForDispatchLoop?: number;
191
192
  createTaskFromStoryBodyCheckboxEnabled?: boolean;
192
193
  queryToAddProjectEnabled?: boolean;
193
194
  queryToAddProject?: string | null;
@@ -468,6 +469,7 @@ ${JSON.stringify(e)}
468
469
  preparationProcessCheckCommand:
469
470
  input.startPreparation.preparationProcessCheckCommand,
470
471
  thresholdForAutoReject: input.thresholdForAutoReject ?? 3,
472
+ thresholdForDispatchLoop: input.thresholdForDispatchLoop,
471
473
  awLogDirectoryPath: input.startPreparation.awLogDirectoryPath,
472
474
  awLogStaleThresholdMinutes:
473
475
  input.startPreparation.awLogStaleThresholdMinutes,
@@ -1079,6 +1079,104 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
1079
1079
  );
1080
1080
  });
1081
1081
 
1082
+ it('should escalate to Failed Preparation when two agents keep naming each other and each one reports every round', async () => {
1083
+ const issue = createMockIssue({
1084
+ url: 'https://github.com/user/repo/issues/1',
1085
+ status: 'Preparation',
1086
+ agent: 'systems-analyst',
1087
+ });
1088
+ const namesReviewer = createMockComment({
1089
+ content:
1090
+ 'From: :robot: systems-analyst\n```json\n{"nextStepAgent": "system-design-reviewer"}\n```',
1091
+ });
1092
+ const namesAnalyst = createMockComment({
1093
+ content:
1094
+ 'From: :robot: system-design-reviewer\n```json\n{"nextStepAgent": "systems-analyst"}\n```',
1095
+ });
1096
+
1097
+ mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
1098
+ mockIssueRepository.get.mockResolvedValue(issue);
1099
+ mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
1100
+ namesReviewer,
1101
+ namesAnalyst,
1102
+ namesReviewer,
1103
+ namesAnalyst,
1104
+ namesReviewer,
1105
+ ]);
1106
+
1107
+ await useCase.run({
1108
+ projectUrl: 'https://github.com/users/user/projects/1',
1109
+ issueUrl: 'https://github.com/user/repo/issues/1',
1110
+ thresholdForAutoReject: 3,
1111
+ thresholdForDispatchLoop: 3,
1112
+ workflowBlockerResolvedWebhookUrl: null,
1113
+ allowedIssueAuthors: null,
1114
+ });
1115
+
1116
+ expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
1117
+ mockProject,
1118
+ expect.anything(),
1119
+ 'failed-preparation-id',
1120
+ );
1121
+ expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
1122
+ expect.anything(),
1123
+ expect.stringContaining(
1124
+ 'dispatched 3 times since the last human comment',
1125
+ ),
1126
+ );
1127
+ });
1128
+
1129
+ it('should keep dispatching when a human comment separates the repeated dispatches', async () => {
1130
+ const issue = createMockIssue({
1131
+ url: 'https://github.com/user/repo/issues/1',
1132
+ status: 'Preparation',
1133
+ agent: 'systems-analyst',
1134
+ });
1135
+ const namesReviewer = createMockComment({
1136
+ content:
1137
+ 'From: :robot: systems-analyst\n```json\n{"nextStepAgent": "system-design-reviewer"}\n```',
1138
+ });
1139
+ const namesAnalyst = createMockComment({
1140
+ content:
1141
+ 'From: :robot: system-design-reviewer\n```json\n{"nextStepAgent": "systems-analyst"}\n```',
1142
+ });
1143
+
1144
+ mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
1145
+ mockIssueRepository.get.mockResolvedValue(issue);
1146
+ mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
1147
+ namesReviewer,
1148
+ namesAnalyst,
1149
+ createMockComment({ content: 'Please take the second option.' }),
1150
+ namesReviewer,
1151
+ namesAnalyst,
1152
+ namesReviewer,
1153
+ ]);
1154
+
1155
+ await useCase.run({
1156
+ projectUrl: 'https://github.com/users/user/projects/1',
1157
+ issueUrl: 'https://github.com/user/repo/issues/1',
1158
+ thresholdForAutoReject: 3,
1159
+ thresholdForDispatchLoop: 3,
1160
+ workflowBlockerResolvedWebhookUrl: null,
1161
+ allowedIssueAuthors: null,
1162
+ });
1163
+
1164
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalledWith(
1165
+ mockProject,
1166
+ expect.anything(),
1167
+ 'failed-preparation-id',
1168
+ );
1169
+ expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
1170
+ mockProject,
1171
+ expect.anything(),
1172
+ 'awaiting-workspace-id',
1173
+ );
1174
+ expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
1175
+ expect.anything(),
1176
+ expect.stringContaining('(2/3)'),
1177
+ );
1178
+ });
1179
+
1082
1180
  it('should not modify labels when nextStepAgent is specified', async () => {
1083
1181
  const issue = createMockIssue({
1084
1182
  url: 'https://github.com/user/repo/issues/1',
@@ -35,7 +35,10 @@ import {
35
35
  issueReactivationTriggerStartOfTomorrow,
36
36
  } from './issueReactivationTriggerIsPending';
37
37
  import { normalizeReportBody } from './normalizeReportBody';
38
- import { resolveNextStepAgentDispatchRepetition } from './resolveNextStepAgentDispatchRepetition';
38
+ import {
39
+ DEFAULT_THRESHOLD_FOR_DISPATCH_LOOP,
40
+ resolveNextStepAgentDispatchRepetition,
41
+ } from './resolveNextStepAgentDispatchRepetition';
39
42
 
40
43
  export class IssueNotFoundError extends Error {
41
44
  constructor(issueUrl: string) {
@@ -106,6 +109,7 @@ export class NotifyFinishedIssuePreparationUseCase {
106
109
  projectUrl: string;
107
110
  issueUrl: string;
108
111
  thresholdForAutoReject: number;
112
+ thresholdForDispatchLoop?: number;
109
113
  workflowBlockerResolvedWebhookUrl: string | null;
110
114
  allowedIssueAuthors?: string[] | null;
111
115
  labelsAsLlmAgentName?: string[] | null;
@@ -246,16 +250,16 @@ export class NotifyFinishedIssuePreparationUseCase {
246
250
  const nextStepAgent = lastAgentReport
247
251
  ? extractNextStepAgent(lastAgentReport.content)
248
252
  : null;
249
- const commentsAfterLastAgentReport = lastAgentReport
250
- ? comments.slice(comments.indexOf(lastAgentReport) + 1)
251
- : [];
252
253
  if (nextStepAgent !== null) {
253
254
  const repetition = resolveNextStepAgentDispatchRepetition({
254
255
  agentFieldValue: issue.agent,
255
256
  nextStepAgent,
256
- commentsAfterLastAgentReport,
257
+ comments,
257
258
  isTrustedAuthor,
258
259
  thresholdForAutoReject: params.thresholdForAutoReject,
260
+ thresholdForDispatchLoop:
261
+ params.thresholdForDispatchLoop ??
262
+ DEFAULT_THRESHOLD_FOR_DISPATCH_LOOP,
259
263
  });
260
264
  if (repetition.type === 'escalateToFailedPreparation') {
261
265
  issue.status = FAILED_PREPARATION_STATUS_NAME;
@@ -27,7 +27,10 @@ import { findLastAgentReport } from './findLastAgentReport';
27
27
  import { isAgentReportBody } from './isAgentReportBody';
28
28
  import { ensureAgentOptionAndGetId } from './ensureAgentOptionAndGetId';
29
29
  import { normalizeReportBody } from './normalizeReportBody';
30
- import { resolveNextStepAgentDispatchRepetition } from './resolveNextStepAgentDispatchRepetition';
30
+ import {
31
+ DEFAULT_THRESHOLD_FOR_DISPATCH_LOOP,
32
+ resolveNextStepAgentDispatchRepetition,
33
+ } from './resolveNextStepAgentDispatchRepetition';
31
34
 
32
35
  const ORPHANED_PREPARATION_REJECTION_DETAIL = 'ORPHANED_PREPARATION';
33
36
 
@@ -64,6 +67,7 @@ export class RevertOrphanedPreparationUseCase {
64
67
  projectUrl: string;
65
68
  preparationProcessCheckCommand: string;
66
69
  thresholdForAutoReject: number;
70
+ thresholdForDispatchLoop?: number;
67
71
  awLogDirectoryPath?: string;
68
72
  awLogStaleThresholdMinutes?: number;
69
73
  awaitingQualityCheckStatus?: string | null;
@@ -136,15 +140,16 @@ export class RevertOrphanedPreparationUseCase {
136
140
  const repetition = resolveNextStepAgentDispatchRepetition({
137
141
  agentFieldValue: issue.agent,
138
142
  nextStepAgent,
139
- commentsAfterLastAgentReport: lastAgentReport
140
- ? comments.slice(comments.indexOf(lastAgentReport) + 1)
141
- : [],
143
+ comments,
142
144
  isTrustedAuthor: (author) =>
143
145
  isAuthorAuthorizedForAutoStatusCheck(
144
146
  author,
145
147
  params.allowedIssueAuthors,
146
148
  ),
147
149
  thresholdForAutoReject: params.thresholdForAutoReject,
150
+ thresholdForDispatchLoop:
151
+ params.thresholdForDispatchLoop ??
152
+ DEFAULT_THRESHOLD_FOR_DISPATCH_LOOP,
148
153
  });
149
154
  if (
150
155
  repetition.type === 'escalateToFailedPreparation' &&
@@ -838,6 +838,94 @@ describe('TriagerApprovalDispatchUseCase', () => {
838
838
  );
839
839
  });
840
840
 
841
+ it('should skip a candidate issue when getCommentsFromIssue throws and continue processing the remaining issues', async () => {
842
+ const failingIssue = createMockIssue({
843
+ number: 1,
844
+ url: 'https://github.com/owner/repo/issues/1',
845
+ itemId: 'item-1',
846
+ status: 'Awaiting Quality Check',
847
+ author: 'owner-user',
848
+ createdAt: new Date('2000-01-01T00:00:00Z'),
849
+ });
850
+ const successIssue = createMockIssue({
851
+ number: 2,
852
+ url: 'https://github.com/owner/repo/issues/2',
853
+ itemId: 'item-2',
854
+ status: 'Awaiting Quality Check',
855
+ author: 'owner-user',
856
+ createdAt: new Date('2000-01-02T00:00:00Z'),
857
+ });
858
+ mockIssueRepository.getAllIssues.mockResolvedValue({
859
+ project: mockProject,
860
+ issues: [failingIssue, successIssue],
861
+ cacheUsed: false,
862
+ });
863
+ mockIssueCommentRepository.getCommentsFromIssue.mockImplementation(
864
+ async (issue) => {
865
+ if (issue.number === 1) {
866
+ throw new Error(
867
+ 'Failed to fetch comments from GitHub REST API: 404 ',
868
+ );
869
+ }
870
+ return [
871
+ createComment(
872
+ 'bot',
873
+ TRIAGER_PROPOSAL_COMMENT(
874
+ 'developer',
875
+ 'regular / workflow improvement',
876
+ false,
877
+ ),
878
+ ),
879
+ createComment('owner-user', 'ok'),
880
+ ];
881
+ },
882
+ );
883
+
884
+ await useCase.run({
885
+ projectUrl: 'https://github.com/orgs/owner/projects/1',
886
+ allowedIssueAuthors: ['owner-user'],
887
+ });
888
+
889
+ expect(
890
+ mockIssueCommentRepository.getCommentsFromIssue,
891
+ ).toHaveBeenCalledTimes(2);
892
+ expect(mockIssueRepository.setIssueAgentField).toHaveBeenCalledTimes(1);
893
+ expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
894
+ mockProject,
895
+ successIssue,
896
+ 'awaiting-workspace-id',
897
+ );
898
+ });
899
+
900
+ it('should propagate non-404 errors from getCommentsFromIssue', async () => {
901
+ const issue = createMockIssue({
902
+ number: 1,
903
+ url: 'https://github.com/owner/repo/issues/1',
904
+ itemId: 'item-1',
905
+ status: 'Awaiting Quality Check',
906
+ author: 'owner-user',
907
+ createdAt: new Date('2000-01-01T00:00:00Z'),
908
+ });
909
+ mockIssueRepository.getAllIssues.mockResolvedValue({
910
+ project: mockProject,
911
+ issues: [issue],
912
+ cacheUsed: false,
913
+ });
914
+ const authError = new Error(
915
+ 'Failed to fetch comments from GitHub REST API: 401 Unauthorized',
916
+ );
917
+ mockIssueCommentRepository.getCommentsFromIssue.mockRejectedValue(
918
+ authError,
919
+ );
920
+
921
+ await expect(
922
+ useCase.run({
923
+ projectUrl: 'https://github.com/orgs/owner/projects/1',
924
+ allowedIssueAuthors: ['owner-user'],
925
+ }),
926
+ ).rejects.toThrow(authError);
927
+ });
928
+
841
929
  it('should use the last triager proposal when multiple proposals exist', async () => {
842
930
  const issue = createMockIssue({
843
931
  status: 'Awaiting Quality Check',
@@ -1,6 +1,7 @@
1
1
  import { IssueCommentRepository } from './adapter-interfaces/IssueCommentRepository';
2
2
  import { IssueRepository } from './adapter-interfaces/IssueRepository';
3
3
  import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
4
+ import { Comment } from '../entities/Comment';
4
5
  import {
5
6
  isAgentReportBody,
6
7
  isAgentReportBodyFromAgent,
@@ -68,6 +69,9 @@ const parseTriagerProposalBlock = (
68
69
  return null;
69
70
  };
70
71
 
72
+ const isNotFoundError = (error: unknown): boolean =>
73
+ (error instanceof Error ? error.message : String(error)).includes('404');
74
+
71
75
  const isApprovalComment = (
72
76
  content: string,
73
77
  author: string,
@@ -160,8 +164,19 @@ export class TriagerApprovalDispatchUseCase {
160
164
  const issue =
161
165
  candidateIssues[(windowStart + slot) % candidateIssues.length];
162
166
 
163
- const comments =
164
- await this.issueCommentRepository.getCommentsFromIssue(issue);
167
+ let comments: Comment[];
168
+ try {
169
+ comments =
170
+ await this.issueCommentRepository.getCommentsFromIssue(issue);
171
+ } catch (error) {
172
+ if (!isNotFoundError(error)) {
173
+ throw error;
174
+ }
175
+ console.warn(
176
+ `[TriagerApprovalDispatch] Failed to fetch comments, skipping issue for this cycle. issueUrl: ${issue.url} error: ${error instanceof Error ? error.message : String(error)}`,
177
+ );
178
+ continue;
179
+ }
165
180
 
166
181
  let firstProposalIndex = -1;
167
182
  let proposal: TriagerProposal | null = null;
@@ -0,0 +1,90 @@
1
+ import { AUTO_STATUS_CHECK_MESSAGE_HEAD } from './autoStatusCheckComments';
2
+ import { isHumanComment } from './isHumanComment';
3
+ import { NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD } from './nextStepAgentDispatchRepeatedMessage';
4
+ import { RETURNED_TO_AWAITING_WORKSPACE_MESSAGE_HEAD } from './returnedToAwaitingWorkspaceMessage';
5
+
6
+ const trustAll = (): boolean => true;
7
+
8
+ describe('isHumanComment', () => {
9
+ it('treats a plain comment from a trusted author as human input', () => {
10
+ expect(
11
+ isHumanComment(
12
+ { author: 'bot', content: 'Please use the second option.' },
13
+ trustAll,
14
+ ),
15
+ ).toBe(true);
16
+ });
17
+
18
+ it('treats an agent report as machine generated', () => {
19
+ expect(
20
+ isHumanComment(
21
+ {
22
+ author: 'bot',
23
+ content: 'From: :robot: agent (model)\n\nReport body.',
24
+ },
25
+ trustAll,
26
+ ),
27
+ ).toBe(false);
28
+ });
29
+
30
+ it('treats an agent report whose leading fenced block precedes the prefix as machine generated', () => {
31
+ expect(
32
+ isHumanComment(
33
+ {
34
+ author: 'bot',
35
+ content:
36
+ '```json\n{ "nextStepAgent": "developer" }\n```\n\nFrom: :robot: agent (model)\n\nReport body.',
37
+ },
38
+ trustAll,
39
+ ),
40
+ ).toBe(false);
41
+ });
42
+
43
+ it('treats the dispatch repetition comment as machine generated', () => {
44
+ expect(
45
+ isHumanComment(
46
+ {
47
+ author: 'bot',
48
+ content: `${NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD} developer\n\nDispatching it again.`,
49
+ },
50
+ trustAll,
51
+ ),
52
+ ).toBe(false);
53
+ });
54
+
55
+ it('treats an auto status check comment as machine generated', () => {
56
+ expect(
57
+ isHumanComment(
58
+ {
59
+ author: 'bot',
60
+ content: `${AUTO_STATUS_CHECK_MESSAGE_HEAD} PULL_REQUEST_NOT_FOUND`,
61
+ },
62
+ trustAll,
63
+ ),
64
+ ).toBe(false);
65
+ });
66
+
67
+ it('treats the returned to awaiting workspace comment as machine generated', () => {
68
+ expect(
69
+ isHumanComment(
70
+ {
71
+ author: 'bot',
72
+ content: `${RETURNED_TO_AWAITING_WORKSPACE_MESSAGE_HEAD}\nThe last report declared that this task needs no pull request.`,
73
+ },
74
+ trustAll,
75
+ ),
76
+ ).toBe(false);
77
+ });
78
+
79
+ it('treats any comment from an untrusted author as human input', () => {
80
+ expect(
81
+ isHumanComment(
82
+ {
83
+ author: 'stranger',
84
+ content: 'From: :robot: agent (model)\n\nImpersonated report.',
85
+ },
86
+ (author: string): boolean => author === 'bot',
87
+ ),
88
+ ).toBe(true);
89
+ });
90
+ });
@@ -0,0 +1,23 @@
1
+ import { AUTO_STATUS_CHECK_MESSAGE_HEAD } from './autoStatusCheckComments';
2
+ import { isAgentReportBody } from './isAgentReportBody';
3
+ import { NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD } from './nextStepAgentDispatchRepeatedMessage';
4
+
5
+ const MACHINE_GENERATED_COMMENT_HEADS = [
6
+ AUTO_STATUS_CHECK_MESSAGE_HEAD,
7
+ NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD,
8
+ ];
9
+
10
+ export const isHumanComment = (
11
+ comment: { author: string; content: string },
12
+ isTrustedAuthor: (author: string) => boolean,
13
+ ): boolean => {
14
+ if (!isTrustedAuthor(comment.author)) {
15
+ return true;
16
+ }
17
+ if (isAgentReportBody(comment.content)) {
18
+ return false;
19
+ }
20
+ return !MACHINE_GENERATED_COMMENT_HEADS.some((head) =>
21
+ comment.content.startsWith(head),
22
+ );
23
+ };
@@ -0,0 +1,2 @@
1
+ export const NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD =
2
+ 'Next step agent dispatch repeated:';