github-issue-tower-defence-management 1.185.0 → 1.186.0

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 (150) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +2 -0
  3. package/bin/adapter/entry-points/cli/index.js +2 -1
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/console/consoleOperationApi.js +43 -1
  6. package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
  7. package/bin/adapter/entry-points/console/consoleProjectResolver.js +7 -1
  8. package/bin/adapter/entry-points/console/consoleProjectResolver.js.map +1 -1
  9. package/bin/adapter/entry-points/console/ui-dist/assets/index-wj6o8qvo.js +86 -0
  10. package/bin/adapter/entry-points/console/ui-dist/assets/index-yZy1TUOo.css +2 -0
  11. package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
  12. package/bin/adapter/entry-points/console/webServer.js +3 -0
  13. package/bin/adapter/entry-points/console/webServer.js.map +1 -1
  14. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +5 -1
  15. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  16. package/bin/adapter/entry-points/handlers/tokenStatusWriter.js +1 -0
  17. package/bin/adapter/entry-points/handlers/tokenStatusWriter.js.map +1 -1
  18. package/bin/adapter/repositories/ProcClaudeHandoverSessionRepository.js +40 -3
  19. package/bin/adapter/repositories/ProcClaudeHandoverSessionRepository.js.map +1 -1
  20. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +36 -0
  21. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  22. package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js +12 -0
  23. package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js.map +1 -1
  24. package/bin/domain/entities/Project.js +11 -1
  25. package/bin/domain/entities/Project.js.map +1 -1
  26. package/bin/domain/usecases/HandleScheduledEventUseCase.js +12 -1
  27. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  28. package/bin/domain/usecases/IssueNoStatusUpdateUseCase.js +36 -0
  29. package/bin/domain/usecases/IssueNoStatusUpdateUseCase.js.map +1 -0
  30. package/bin/domain/usecases/IssueRejectionEvaluator.js +19 -0
  31. package/bin/domain/usecases/IssueRejectionEvaluator.js.map +1 -1
  32. package/bin/domain/usecases/QualityCheckAdvanceUseCase.js +1 -0
  33. package/bin/domain/usecases/QualityCheckAdvanceUseCase.js.map +1 -1
  34. package/bin/domain/usecases/ReopenedDoneIssueRevertUseCase.js +36 -0
  35. package/bin/domain/usecases/ReopenedDoneIssueRevertUseCase.js.map +1 -0
  36. package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js +2 -5
  37. package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js.map +1 -1
  38. package/bin/domain/usecases/TokenExhaustionHandoverUseCase.js +9 -1
  39. package/bin/domain/usecases/TokenExhaustionHandoverUseCase.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/adapter/entry-points/cli/index.ts +14 -10
  42. package/src/adapter/entry-points/console/consoleOperationApi.test.ts +199 -0
  43. package/src/adapter/entry-points/console/consoleOperationApi.ts +65 -0
  44. package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +26 -0
  45. package/src/adapter/entry-points/console/consoleProjectResolver.ts +8 -1
  46. package/src/adapter/entry-points/console/dashboardEmitterComposerKey.test.ts +1 -0
  47. package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +40 -0
  48. package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +18 -0
  49. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.stories.tsx +4 -0
  50. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.test.tsx +114 -141
  51. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.tsx +84 -3
  52. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +23 -0
  53. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +73 -0
  54. package/src/adapter/entry-points/console/ui/src/index.css +35 -0
  55. package/src/adapter/entry-points/console/ui-dist/assets/index-wj6o8qvo.js +86 -0
  56. package/src/adapter/entry-points/console/ui-dist/assets/index-yZy1TUOo.css +2 -0
  57. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  58. package/src/adapter/entry-points/console/webServer.ts +7 -0
  59. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +11 -0
  60. package/src/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.test.ts +1 -0
  61. package/src/adapter/entry-points/handlers/dashboardRowWriter.test.ts +1 -0
  62. package/src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.test.ts +1 -0
  63. package/src/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.test.ts +1 -0
  64. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +1 -0
  65. package/src/adapter/entry-points/handlers/ownerCallFileCleaner.test.ts +1 -0
  66. package/src/adapter/entry-points/handlers/situationFileWriter.test.ts +1 -0
  67. package/src/adapter/entry-points/handlers/staleTmuxSessionCleaner.test.ts +1 -0
  68. package/src/adapter/entry-points/handlers/tokenStatusWriter.test.ts +1 -0
  69. package/src/adapter/entry-points/handlers/tokenStatusWriter.ts +1 -0
  70. package/src/adapter/repositories/GitHubIssueCommentRepository.test.ts +1 -0
  71. package/src/adapter/repositories/ProcClaudeHandoverSessionRepository.test.ts +48 -0
  72. package/src/adapter/repositories/ProcClaudeHandoverSessionRepository.ts +55 -2
  73. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +90 -0
  74. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +48 -2
  75. package/src/adapter/repositories/issue/GraphqlProjectItemRepository.test.ts +107 -0
  76. package/src/adapter/repositories/issue/GraphqlProjectItemRepository.ts +20 -0
  77. package/src/adapter/repositories/issue/RestIssueRepository.test.ts +1 -0
  78. package/src/domain/entities/ClaudeHandoverSession.ts +1 -0
  79. package/src/domain/entities/Issue.ts +1 -0
  80. package/src/domain/entities/Project.ts +11 -2
  81. package/src/domain/usecases/AgentDesignationLabelAdoptUseCase.test.ts +1 -0
  82. package/src/domain/usecases/CheckIssueReviewReadinessUseCase.test.ts +1 -0
  83. package/src/domain/usecases/GetStoryObjectMapUseCase.test.ts +1 -0
  84. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +144 -0
  85. package/src/domain/usecases/HandleScheduledEventUseCase.ts +16 -0
  86. package/src/domain/usecases/InTmuxByHumanSessionTokenCountUseCase.test.ts +1 -0
  87. package/src/domain/usecases/IssueNoStatusUpdateUseCase.test.ts +165 -0
  88. package/src/domain/usecases/IssueNoStatusUpdateUseCase.ts +44 -0
  89. package/src/domain/usecases/IssueRejectionEvaluator.test.ts +120 -0
  90. package/src/domain/usecases/IssueRejectionEvaluator.ts +21 -0
  91. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +1 -0
  92. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +1 -0
  93. package/src/domain/usecases/QualityCheckAdvanceUseCase.test.ts +34 -0
  94. package/src/domain/usecases/QualityCheckAdvanceUseCase.ts +1 -0
  95. package/src/domain/usecases/ReopenedDoneIssueRevertUseCase.test.ts +188 -0
  96. package/src/domain/usecases/ReopenedDoneIssueRevertUseCase.ts +57 -0
  97. package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.test.ts +44 -0
  98. package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.ts +4 -12
  99. package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +1 -0
  100. package/src/domain/usecases/SetupTowerDefenceProjectUseCase.test.ts +1 -0
  101. package/src/domain/usecases/StaleTmuxSessionKillUseCase.test.ts +1 -0
  102. package/src/domain/usecases/StartPreparationUseCase.test.ts +1 -0
  103. package/src/domain/usecases/TokenExhaustionHandoverUseCase.test.ts +59 -1
  104. package/src/domain/usecases/TokenExhaustionHandoverUseCase.ts +14 -1
  105. package/src/domain/usecases/TriagerApprovalDispatchUseCase.test.ts +1 -0
  106. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +5 -0
  107. package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +1 -0
  108. package/src/domain/usecases/dashboard/GenerateDashboardRowUseCase.test.ts +1 -0
  109. package/src/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.test.ts +1 -0
  110. package/src/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.test.ts +1 -0
  111. package/src/domain/usecases/issueReactivationTriggerIsPending.test.ts +1 -0
  112. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  113. package/types/adapter/entry-points/console/consoleOperationApi.d.ts +2 -0
  114. package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
  115. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts +1 -0
  116. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts.map +1 -1
  117. package/types/adapter/entry-points/console/webServer.d.ts +2 -0
  118. package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
  119. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  120. package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts.map +1 -1
  121. package/types/adapter/repositories/ProcClaudeHandoverSessionRepository.d.ts +1 -0
  122. package/types/adapter/repositories/ProcClaudeHandoverSessionRepository.d.ts.map +1 -1
  123. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +4 -1
  124. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  125. package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts +1 -0
  126. package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts.map +1 -1
  127. package/types/domain/entities/ClaudeHandoverSession.d.ts +1 -0
  128. package/types/domain/entities/ClaudeHandoverSession.d.ts.map +1 -1
  129. package/types/domain/entities/Issue.d.ts +1 -0
  130. package/types/domain/entities/Issue.d.ts.map +1 -1
  131. package/types/domain/entities/Project.d.ts +2 -1
  132. package/types/domain/entities/Project.d.ts.map +1 -1
  133. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +6 -1
  134. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  135. package/types/domain/usecases/IssueNoStatusUpdateUseCase.d.ts +12 -0
  136. package/types/domain/usecases/IssueNoStatusUpdateUseCase.d.ts.map +1 -0
  137. package/types/domain/usecases/IssueRejectionEvaluator.d.ts +1 -0
  138. package/types/domain/usecases/IssueRejectionEvaluator.d.ts.map +1 -1
  139. package/types/domain/usecases/QualityCheckAdvanceUseCase.d.ts.map +1 -1
  140. package/types/domain/usecases/ReopenedDoneIssueRevertUseCase.d.ts +12 -0
  141. package/types/domain/usecases/ReopenedDoneIssueRevertUseCase.d.ts.map +1 -0
  142. package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts.map +1 -1
  143. package/types/domain/usecases/TokenExhaustionHandoverUseCase.d.ts +1 -0
  144. package/types/domain/usecases/TokenExhaustionHandoverUseCase.d.ts.map +1 -1
  145. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +3 -0
  146. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
  147. package/bin/adapter/entry-points/console/ui-dist/assets/index-8Ni5iu-C.css +0 -2
  148. package/bin/adapter/entry-points/console/ui-dist/assets/index-DQT4HL0G.js +0 -86
  149. package/src/adapter/entry-points/console/ui-dist/assets/index-8Ni5iu-C.css +0 -2
  150. package/src/adapter/entry-points/console/ui-dist/assets/index-DQT4HL0G.js +0 -86
@@ -9,7 +9,7 @@ import {
9
9
  PullRequestReviewCommentSide,
10
10
  PullRequestReviewInlineLocation,
11
11
  } from '../../../domain/usecases/adapter-interfaces/IssueRepository';
12
- import { Project } from '../../../domain/entities/Project';
12
+ import { FieldOption, Project } from '../../../domain/entities/Project';
13
13
  import { Issue } from '../../../domain/entities/Issue';
14
14
  import { SearchedIssue } from '../../../domain/entities/SearchedIssue';
15
15
  import { StoryObjectMap } from '../../../domain/entities/StoryObjectMap';
@@ -28,7 +28,10 @@ import {
28
28
  ProjectIssuesCacheRepository,
29
29
  } from '../ProjectIssuesCacheRepository';
30
30
  import { BaseGitHubRepository } from '../BaseGitHubRepository';
31
- import { fetchGithubGraphql } from '../githubGraphqlClient';
31
+ import {
32
+ fetchGithubGraphql,
33
+ postGithubGraphqlJson,
34
+ } from '../githubGraphqlClient';
32
35
  import { normalizeFieldName } from '../utils';
33
36
  import { LocalStorageRepository } from '../LocalStorageRepository';
34
37
  import { Member } from '../../../domain/entities/Member';
@@ -687,6 +690,7 @@ export class ApiV3CheerioRestIssueRepository
687
690
  closingIssueReferenceUrls: item.closingIssueReferenceUrls,
688
691
  agent,
689
692
  isRepoArchived: item.isRepoArchived,
693
+ stateReason: item.stateReason,
690
694
  };
691
695
  };
692
696
  private restoreIssuesFromCache = (rawIssues: unknown): Issue[] | null => {
@@ -723,12 +727,20 @@ export class ApiV3CheerioRestIssueRepository
723
727
  ? issue.closingIssueReferenceUrls
724
728
  : [];
725
729
 
730
+ const stateReason =
731
+ 'stateReason' in issue &&
732
+ (issue.stateReason === 'COMPLETED' ||
733
+ issue.stateReason === 'NOT_PLANNED' ||
734
+ issue.stateReason === 'REOPENED')
735
+ ? issue.stateReason
736
+ : null;
726
737
  return {
727
738
  ...issue,
728
739
  nextActionDate: nextActionDate,
729
740
  completionDate50PercentConfidence: completionDate50PercentConfidence,
730
741
  createdAt: createdAt,
731
742
  closingIssueReferenceUrls: closingIssueReferenceUrls,
743
+ stateReason: stateReason,
732
744
  };
733
745
  });
734
746
 
@@ -1071,6 +1083,40 @@ export class ApiV3CheerioRestIssueRepository
1071
1083
  { singleSelectOptionId: storyOptionId },
1072
1084
  );
1073
1085
  };
1086
+ updateStoryOptionColor = async (
1087
+ project: Project & { story: NonNullable<Project['story']> },
1088
+ storyOptionId: string,
1089
+ newColor: FieldOption['color'],
1090
+ ): Promise<void> => {
1091
+ const mutation = `mutation UpdateStoryOptionColor($fieldId: ID!, $options: [ProjectV2SingleSelectFieldOptionInput!]!) {
1092
+ updateProjectV2Field(input: {
1093
+ fieldId: $fieldId
1094
+ singleSelectOptions: $options
1095
+ }) {
1096
+ projectV2Field {
1097
+ ... on ProjectV2SingleSelectField {
1098
+ options { id name color description }
1099
+ }
1100
+ }
1101
+ }
1102
+ }`;
1103
+ const options = project.story.stories.map((opt) => ({
1104
+ id: opt.id,
1105
+ name: opt.name,
1106
+ color: opt.id === storyOptionId ? newColor : opt.color,
1107
+ description: opt.description,
1108
+ }));
1109
+ const response = await postGithubGraphqlJson<{
1110
+ errors?: { message: string }[];
1111
+ }>({
1112
+ ghToken: this.ghToken,
1113
+ query: mutation,
1114
+ variables: { fieldId: project.story.fieldId, options },
1115
+ });
1116
+ if (response.errors && response.errors.length > 0) {
1117
+ throw new Error(response.errors.map((e) => e.message).join('; '));
1118
+ }
1119
+ };
1074
1120
  clearProjectField = async (
1075
1121
  project: Project,
1076
1122
  fieldId: string,
@@ -781,6 +781,80 @@ describe('GraphqlProjectItemRepository', () => {
781
781
  );
782
782
  warnSpy.mockRestore();
783
783
  });
784
+
785
+ it('requests stateReason in the Issue content fragment of the GraphQL query', async () => {
786
+ const repository = new GraphqlProjectItemRepository(
787
+ new LocalStorageRepository(),
788
+ 'dummy-token',
789
+ );
790
+ mockPost.mockReturnValueOnce(makePageResponse(false, 'cursor-1', 1));
791
+
792
+ await repository.fetchProjectItems('test-project-id');
793
+
794
+ const sentQuery = extractRequestedQueryFromMockCall(
795
+ mockPost.mock.calls[0],
796
+ );
797
+ expect(sentQuery).toContain('stateReason');
798
+ });
799
+
800
+ it('maps stateReason REOPENED from Issue content node to ProjectItem.stateReason', async () => {
801
+ const repository = new GraphqlProjectItemRepository(
802
+ new LocalStorageRepository(),
803
+ 'dummy-token',
804
+ );
805
+ mockPost.mockReturnValueOnce(
806
+ mockJsonResponse({
807
+ data: {
808
+ node: {
809
+ items: {
810
+ totalCount: 1,
811
+ pageInfo: {
812
+ endCursor: 'cursor-1',
813
+ startCursor: 'cursor-start',
814
+ hasNextPage: false,
815
+ },
816
+ nodes: [
817
+ {
818
+ id: 'item-1',
819
+ fieldValues: { nodes: [] },
820
+ content: {
821
+ repository: { nameWithOwner: 'owner/repo' },
822
+ number: 1,
823
+ title: 'Test Issue',
824
+ state: 'OPEN',
825
+ stateReason: 'REOPENED',
826
+ url: 'https://github.com/owner/repo/issues/1',
827
+ body: 'body',
828
+ createdAt: '2024-01-01T00:00:00Z',
829
+ labels: { nodes: [] },
830
+ assignees: { nodes: [] },
831
+ },
832
+ },
833
+ ],
834
+ },
835
+ },
836
+ },
837
+ }),
838
+ );
839
+
840
+ const result = await repository.fetchProjectItems('test-project-id');
841
+
842
+ expect(result).toHaveLength(1);
843
+ expect(result[0].stateReason).toBe('REOPENED');
844
+ });
845
+
846
+ it('maps stateReason to null when the field is absent from the content node', async () => {
847
+ const repository = new GraphqlProjectItemRepository(
848
+ new LocalStorageRepository(),
849
+ 'dummy-token',
850
+ );
851
+ mockPost.mockReturnValueOnce(makePageResponse(false, 'cursor-1', 1));
852
+
853
+ const result = await repository.fetchProjectItems('test-project-id');
854
+
855
+ expect(result).toHaveLength(1);
856
+ expect(result[0].stateReason).toBeNull();
857
+ });
784
858
  });
785
859
 
786
860
  const extractRequestedVariablesFromMockCall = (
@@ -1665,5 +1739,38 @@ describe('GraphqlProjectItemRepository', () => {
1665
1739
 
1666
1740
  warnSpy.mockRestore();
1667
1741
  });
1742
+
1743
+ it('maps stateReason from the single-item Issue response', async () => {
1744
+ const localStorageRepository = new LocalStorageRepository();
1745
+ const repository = new GraphqlProjectItemRepository(
1746
+ localStorageRepository,
1747
+ 'dummy-token',
1748
+ );
1749
+
1750
+ mockPost.mockReturnValueOnce(
1751
+ mockJsonResponse({
1752
+ data: {
1753
+ repository: {
1754
+ issue: {
1755
+ ...makeContentNode(
1756
+ 'https://github.com/owner/repo/issues/42',
1757
+ 42,
1758
+ 'Reopened Issue',
1759
+ ),
1760
+ stateReason: 'REOPENED',
1761
+ },
1762
+ pullRequest: null,
1763
+ },
1764
+ },
1765
+ }),
1766
+ );
1767
+
1768
+ const result = await repository.fetchProjectItemByUrl(
1769
+ 'https://github.com/owner/repo/issues/42',
1770
+ );
1771
+
1772
+ expect(result).not.toBeNull();
1773
+ expect(result?.stateReason).toBe('REOPENED');
1774
+ });
1668
1775
  });
1669
1776
  });
@@ -18,6 +18,7 @@ export type ProjectItem = {
18
18
  author: string;
19
19
  closingIssueReferenceUrls: string[];
20
20
  isRepoArchived: boolean;
21
+ stateReason: 'COMPLETED' | 'NOT_PLANNED' | 'REOPENED' | null;
21
22
  customFields: {
22
23
  name: string;
23
24
  value: string | null;
@@ -51,6 +52,7 @@ type ProjectV2ItemContentNode = {
51
52
  labels: { nodes: { name: string }[] };
52
53
  assignees: { nodes: { login: string }[] };
53
54
  closingIssuesReferences?: { nodes: { url: string }[] };
55
+ stateReason?: string | null;
54
56
  };
55
57
  type ProjectV2ItemNode = {
56
58
  id: string;
@@ -121,6 +123,7 @@ const PROJECT_V2_ITEM_FIELD_VALUES_AND_CONTENT_SELECTION = `
121
123
  number
122
124
  title
123
125
  state
126
+ stateReason
124
127
  url
125
128
  body
126
129
  createdAt
@@ -186,6 +189,19 @@ export const RATE_LIMIT_MAX_BACKOFF_MS = 300000;
186
189
  const sleep = (ms: number): Promise<void> =>
187
190
  new Promise((resolve) => setTimeout(resolve, ms));
188
191
 
192
+ const toStateReason = (
193
+ value: string | null | undefined,
194
+ ): 'COMPLETED' | 'NOT_PLANNED' | 'REOPENED' | null => {
195
+ if (
196
+ value === 'COMPLETED' ||
197
+ value === 'NOT_PLANNED' ||
198
+ value === 'REOPENED'
199
+ ) {
200
+ return value;
201
+ }
202
+ return null;
203
+ };
204
+
189
205
  const parseNonNegativeIntegerHeader = (value: string | null): number | null => {
190
206
  if (value === null) {
191
207
  return null;
@@ -634,6 +650,7 @@ query GetProjectItems($projectId: ID!, $after: String, $first: Int!, $query: Str
634
650
  ?.map((node) => node.url)
635
651
  .filter((url) => url.length > 0) || [],
636
652
  isRepoArchived: item.content.repository.isArchived ?? false,
653
+ stateReason: toStateReason(item.content.stateReason),
637
654
  customFields: item.fieldValues.nodes
638
655
  .filter((field) => !!field.field)
639
656
  .map((field) => {
@@ -1049,6 +1066,7 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
1049
1066
  number
1050
1067
  title
1051
1068
  state
1069
+ stateReason
1052
1070
  url
1053
1071
  body
1054
1072
  createdAt
@@ -1222,6 +1240,7 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
1222
1240
  number: number;
1223
1241
  title: string;
1224
1242
  state: string;
1243
+ stateReason?: string | null;
1225
1244
  url: string;
1226
1245
  body: string;
1227
1246
  createdAt: string;
@@ -1309,6 +1328,7 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
1309
1328
  ?.map((node) => node.url)
1310
1329
  .filter((url) => url.length > 0) || [],
1311
1330
  isRepoArchived: content.repository.isArchived ?? false,
1331
+ stateReason: toStateReason(content.stateReason),
1312
1332
  customFields: item.fieldValues.nodes
1313
1333
  .filter((field) => !!field.field)
1314
1334
  .map((field) => {
@@ -52,6 +52,7 @@ const buildIssue = (overrides: Partial<Issue> = {}): Issue => ({
52
52
  author: '',
53
53
  closingIssueReferenceUrls: [],
54
54
  agent: null,
55
+ stateReason: null,
55
56
  ...overrides,
56
57
  });
57
58
 
@@ -8,4 +8,5 @@ export type ClaudeHandoverSession = {
8
8
  sessionName: string | null;
9
9
  name: string | null;
10
10
  issueUrl: string | null;
11
+ runsUnderWorkspacePreparationScript: boolean;
11
12
  };
@@ -27,4 +27,5 @@ export type Issue = {
27
27
  closingIssueReferenceUrls: string[];
28
28
  agent: string | null;
29
29
  isRepoArchived?: boolean;
30
+ stateReason: 'COMPLETED' | 'NOT_PLANNED' | 'REOPENED' | null;
30
31
  };
@@ -1,9 +1,18 @@
1
+ export const FIELD_OPTION_COLORS = [
2
+ 'GRAY',
3
+ 'BLUE',
4
+ 'GREEN',
5
+ 'YELLOW',
6
+ 'ORANGE',
7
+ 'RED',
8
+ 'PINK',
9
+ 'PURPLE',
10
+ ] as const;
1
11
  export type StoryOption = FieldOption;
2
12
  export type FieldOption = {
3
13
  id: string;
4
14
  name: string;
5
- color:
6
- 'GRAY' | 'BLUE' | 'GREEN' | 'YELLOW' | 'ORANGE' | 'RED' | 'PINK' | 'PURPLE';
15
+ color: (typeof FIELD_OPTION_COLORS)[number];
7
16
  description: string;
8
17
  };
9
18
 
@@ -28,6 +28,7 @@ const createIssue = (overrides: Partial<Issue> = {}): Issue => ({
28
28
  author: 'author',
29
29
  closingIssueReferenceUrls: [],
30
30
  agent: null,
31
+ stateReason: null,
31
32
  ...overrides,
32
33
  });
33
34
 
@@ -29,6 +29,7 @@ const createMockIssue = (overrides: Partial<Issue> = {}): Issue => ({
29
29
  author: 'test-user',
30
30
  closingIssueReferenceUrls: [],
31
31
  agent: null,
32
+ stateReason: null,
32
33
  ...overrides,
33
34
  });
34
35
 
@@ -70,6 +70,7 @@ describe('GetStoryObjectMapUseCase', () => {
70
70
  author: '',
71
71
  closingIssueReferenceUrls: [],
72
72
  agent: null,
73
+ stateReason: null,
73
74
  ...overrides,
74
75
  });
75
76
 
@@ -21,6 +21,7 @@ import { SetNoStoryIssueToStoryUseCase } from './SetNoStoryIssueToStoryUseCase';
21
21
  import { CreateNewStoryByLabelUseCase } from './CreateNewStoryByLabelUseCase';
22
22
  import { AssignNoAssigneeIssueToManagerUseCase } from './AssignNoAssigneeIssueToManagerUseCase';
23
23
  import { UpdateIssueStatusByLabelUseCase } from './UpdateIssueStatusByLabelUseCase';
24
+ import { IssueNoStatusUpdateUseCase } from './IssueNoStatusUpdateUseCase';
24
25
  import { StartPreparationUseCase } from './StartPreparationUseCase';
25
26
  import { RevertOrphanedPreparationUseCase } from './RevertOrphanedPreparationUseCase';
26
27
  import { RevertNotReadyReviewQueueIssueUseCase } from './RevertNotReadyReviewQueueIssueUseCase';
@@ -31,6 +32,7 @@ import { SetupTowerDefenceProjectUseCase } from './SetupTowerDefenceProjectUseCa
31
32
  import { UpdateRateLimitCacheUseCase } from './UpdateRateLimitCacheUseCase';
32
33
  import { DailySecurityScanUseCase } from './DailySecurityScanUseCase';
33
34
  import { QualityCheckAdvanceUseCase } from './QualityCheckAdvanceUseCase';
35
+ import { ReopenedDoneIssueRevertUseCase } from './ReopenedDoneIssueRevertUseCase';
34
36
 
35
37
  describe('HandleScheduledEventUseCase', () => {
36
38
  describe('createTargetDateTimes', () => {
@@ -121,6 +123,7 @@ describe('HandleScheduledEventUseCase', () => {
121
123
  mock<AssignNoAssigneeIssueToManagerUseCase>();
122
124
  const mockUpdateIssueStatusByLabelUseCase =
123
125
  mock<UpdateIssueStatusByLabelUseCase>();
126
+ const mockIssueNoStatusUpdateUseCase = mock<IssueNoStatusUpdateUseCase>();
124
127
  const mockStartPreparationUseCase = mock<StartPreparationUseCase>();
125
128
  const mockRevertOrphanedPreparationUseCase =
126
129
  mock<RevertOrphanedPreparationUseCase>();
@@ -133,6 +136,8 @@ describe('HandleScheduledEventUseCase', () => {
133
136
  const mockUpdateRateLimitCacheUseCase = mock<UpdateRateLimitCacheUseCase>();
134
137
  const mockDailySecurityScanUseCase = mock<DailySecurityScanUseCase>();
135
138
  const mockAdvanceQualityCheckUseCase = mock<QualityCheckAdvanceUseCase>();
139
+ const mockReopenedDoneIssueRevertUseCase =
140
+ mock<ReopenedDoneIssueRevertUseCase>();
136
141
  const mockDateRepository = mock<DateRepository>();
137
142
  const mockSpreadsheetRepository = mock<SpreadsheetRepository>();
138
143
  const mockProjectRepository = mock<ProjectRepository>();
@@ -156,6 +161,7 @@ describe('HandleScheduledEventUseCase', () => {
156
161
  mockCreateNewStoryByLabelUseCase,
157
162
  mockAssignNoAssigneeIssueToManagerUseCase,
158
163
  mockUpdateIssueStatusByLabelUseCase,
164
+ mockIssueNoStatusUpdateUseCase,
159
165
  mockStartPreparationUseCase,
160
166
  mockRevertOrphanedPreparationUseCase,
161
167
  mockRevertNotReadyReviewQueueIssueUseCase,
@@ -164,6 +170,7 @@ describe('HandleScheduledEventUseCase', () => {
164
170
  mockUpdateRateLimitCacheUseCase,
165
171
  mockDailySecurityScanUseCase,
166
172
  mockAdvanceQualityCheckUseCase,
173
+ mockReopenedDoneIssueRevertUseCase,
167
174
  mockDateRepository,
168
175
  mockSpreadsheetRepository,
169
176
  mockProjectRepository,
@@ -240,6 +247,41 @@ describe('HandleScheduledEventUseCase', () => {
240
247
  });
241
248
  });
242
249
 
250
+ it('should call IssueNoStatusUpdateUseCase with project and issues when startPreparation is configured', async () => {
251
+ const input = {
252
+ projectName: 'test-project',
253
+ org: 'test-org',
254
+ projectUrl: 'https://github.com/test-org/test-project',
255
+ manager: 'test-manager',
256
+ workingReport: {
257
+ repo: 'test-repo',
258
+ members: ['member1'],
259
+ spreadsheetUrl: 'https://docs.google.com/spreadsheets/test',
260
+ },
261
+ urlOfStoryView: 'https://github.com/test-org/test-project/issues',
262
+ disabled: false,
263
+ startPreparation: {
264
+ defaultAgentName: 'agent1',
265
+ configFilePath: '/path/to/config.yml',
266
+ maximumPreparingIssuesCount: null,
267
+ },
268
+ };
269
+
270
+ const mockProject = mock<Project>();
271
+ const mockIssues = [mock<Issue>()];
272
+ mockIssueRepository.getAllIssues.mockResolvedValue({
273
+ issues: mockIssues,
274
+ project: mockProject,
275
+ cacheUsed: false,
276
+ });
277
+ await useCase.run(input);
278
+
279
+ expect(mockIssueNoStatusUpdateUseCase.run).toHaveBeenCalledWith({
280
+ project: mockProject,
281
+ issues: mockIssues,
282
+ });
283
+ });
284
+
243
285
  it('should call TriagerApprovalDispatchUseCase with project URL', async () => {
244
286
  const input = {
245
287
  projectName: 'test-project',
@@ -1616,6 +1658,105 @@ describe('HandleScheduledEventUseCase', () => {
1616
1658
  expect(mockStartPreparationUseCase.run).toHaveBeenCalled();
1617
1659
  });
1618
1660
  });
1661
+
1662
+ describe('reopenedDoneIssueRevertUseCase', () => {
1663
+ const baseInput = {
1664
+ projectName: 'test-project',
1665
+ org: 'test-org',
1666
+ projectUrl: 'https://github.com/test-org/test-project',
1667
+ manager: 'test-manager',
1668
+ workingReport: {
1669
+ repo: 'test-repo',
1670
+ members: ['member1'],
1671
+ spreadsheetUrl: 'https://docs.google.com/spreadsheets/test',
1672
+ },
1673
+ urlOfStoryView: 'https://github.com/test-org/test-project/issues',
1674
+ disabled: false,
1675
+ };
1676
+
1677
+ it('does not call reopenedDoneIssueRevertUseCase when startPreparation is absent', async () => {
1678
+ await useCase.run(baseInput);
1679
+
1680
+ expect(mockReopenedDoneIssueRevertUseCase.run).not.toHaveBeenCalled();
1681
+ });
1682
+
1683
+ it('does not call reopenedDoneIssueRevertUseCase when autoRevertReopenedDoneEnabled is absent', async () => {
1684
+ await useCase.run({
1685
+ ...baseInput,
1686
+ startPreparation: {
1687
+ defaultAgentName: 'agent1',
1688
+ configFilePath: '/path/to/config.yml',
1689
+ maximumPreparingIssuesCount: null,
1690
+ autoAdvanceQualityCheckEnabled: false,
1691
+ },
1692
+ });
1693
+
1694
+ expect(mockReopenedDoneIssueRevertUseCase.run).not.toHaveBeenCalled();
1695
+ });
1696
+
1697
+ it('does not call reopenedDoneIssueRevertUseCase when autoRevertReopenedDoneEnabled is false', async () => {
1698
+ await useCase.run({
1699
+ ...baseInput,
1700
+ startPreparation: {
1701
+ defaultAgentName: 'agent1',
1702
+ configFilePath: '/path/to/config.yml',
1703
+ maximumPreparingIssuesCount: null,
1704
+ autoAdvanceQualityCheckEnabled: false,
1705
+ autoRevertReopenedDoneEnabled: false,
1706
+ },
1707
+ });
1708
+
1709
+ expect(mockReopenedDoneIssueRevertUseCase.run).not.toHaveBeenCalled();
1710
+ });
1711
+
1712
+ it('calls reopenedDoneIssueRevertUseCase with project and issues when autoRevertReopenedDoneEnabled is true', async () => {
1713
+ const mockProject = mock<Project>();
1714
+ const mockIssues: Issue[] = [];
1715
+ mockIssueRepository.getAllIssues.mockResolvedValue({
1716
+ issues: mockIssues,
1717
+ project: mockProject,
1718
+ cacheUsed: false,
1719
+ });
1720
+
1721
+ await useCase.run({
1722
+ ...baseInput,
1723
+ startPreparation: {
1724
+ defaultAgentName: 'agent1',
1725
+ configFilePath: '/path/to/config.yml',
1726
+ maximumPreparingIssuesCount: null,
1727
+ autoAdvanceQualityCheckEnabled: false,
1728
+ autoRevertReopenedDoneEnabled: true,
1729
+ },
1730
+ });
1731
+
1732
+ expect(mockReopenedDoneIssueRevertUseCase.run).toHaveBeenCalledWith({
1733
+ project: mockProject,
1734
+ issues: mockIssues,
1735
+ });
1736
+ });
1737
+
1738
+ it('continues to startPreparationUseCase when reopenedDoneIssueRevertUseCase.run rejects', async () => {
1739
+ mockReopenedDoneIssueRevertUseCase.run.mockRejectedValue(
1740
+ new AggregateError(
1741
+ [new Error('GitHub API error')],
1742
+ 'Failed to revert 1 reopened Done issue(s)',
1743
+ ),
1744
+ );
1745
+
1746
+ await useCase.run({
1747
+ ...baseInput,
1748
+ startPreparation: {
1749
+ defaultAgentName: 'agent1',
1750
+ configFilePath: '/path/to/config.yml',
1751
+ maximumPreparingIssuesCount: null,
1752
+ autoAdvanceQualityCheckEnabled: false,
1753
+ autoRevertReopenedDoneEnabled: true,
1754
+ },
1755
+ });
1756
+
1757
+ expect(mockStartPreparationUseCase.run).toHaveBeenCalled();
1758
+ });
1759
+ });
1619
1760
  });
1620
1761
 
1621
1762
  describe('storyIssues', () => {
@@ -1637,6 +1778,7 @@ describe('HandleScheduledEventUseCase', () => {
1637
1778
  mock<CreateNewStoryByLabelUseCase>(),
1638
1779
  mock<AssignNoAssigneeIssueToManagerUseCase>(),
1639
1780
  mock<UpdateIssueStatusByLabelUseCase>(),
1781
+ mock<IssueNoStatusUpdateUseCase>(),
1640
1782
  mock<StartPreparationUseCase>(),
1641
1783
  mock<RevertOrphanedPreparationUseCase>(),
1642
1784
  mock<RevertNotReadyReviewQueueIssueUseCase>(),
@@ -1645,6 +1787,7 @@ describe('HandleScheduledEventUseCase', () => {
1645
1787
  null,
1646
1788
  null,
1647
1789
  mock<QualityCheckAdvanceUseCase>(),
1790
+ mock<ReopenedDoneIssueRevertUseCase>(),
1648
1791
  mock<DateRepository>(),
1649
1792
  mock<SpreadsheetRepository>(),
1650
1793
  mock<ProjectRepository>(),
@@ -1701,6 +1844,7 @@ describe('HandleScheduledEventUseCase', () => {
1701
1844
  author: '',
1702
1845
  closingIssueReferenceUrls: [],
1703
1846
  agent: null,
1847
+ stateReason: null,
1704
1848
  };
1705
1849
 
1706
1850
  it('should return storyIssue as null when the only matching issue is closed', async () => {
@@ -21,6 +21,7 @@ import { SetNoStoryIssueToStoryUseCase } from './SetNoStoryIssueToStoryUseCase';
21
21
  import { CreateNewStoryByLabelUseCase } from './CreateNewStoryByLabelUseCase';
22
22
  import { AssignNoAssigneeIssueToManagerUseCase } from './AssignNoAssigneeIssueToManagerUseCase';
23
23
  import { UpdateIssueStatusByLabelUseCase } from './UpdateIssueStatusByLabelUseCase';
24
+ import { IssueNoStatusUpdateUseCase } from './IssueNoStatusUpdateUseCase';
24
25
  import {
25
26
  RotationOrderEntry,
26
27
  StartPreparationUseCase,
@@ -40,6 +41,7 @@ import {
40
41
  DailySecurityScanUseCase,
41
42
  } from './DailySecurityScanUseCase';
42
43
  import { QualityCheckAdvanceUseCase } from './QualityCheckAdvanceUseCase';
44
+ import { ReopenedDoneIssueRevertUseCase } from './ReopenedDoneIssueRevertUseCase';
43
45
 
44
46
  export class ProjectNotFoundError extends Error {
45
47
  constructor(message: string) {
@@ -133,6 +135,7 @@ export class HandleScheduledEventUseCase {
133
135
  readonly createNewStoryByLabelUseCase: CreateNewStoryByLabelUseCase,
134
136
  readonly assignNoAssigneeIssueToManagerUseCase: AssignNoAssigneeIssueToManagerUseCase,
135
137
  readonly updateIssueStatusByLabelUseCase: UpdateIssueStatusByLabelUseCase,
138
+ readonly issueNoStatusUpdateUseCase: IssueNoStatusUpdateUseCase,
136
139
  readonly startPreparationUseCase: StartPreparationUseCase,
137
140
  readonly revertOrphanedPreparationUseCase: RevertOrphanedPreparationUseCase,
138
141
  readonly revertNotReadyReviewQueueIssueUseCase: RevertNotReadyReviewQueueIssueUseCase,
@@ -141,6 +144,7 @@ export class HandleScheduledEventUseCase {
141
144
  readonly updateRateLimitCacheUseCase: UpdateRateLimitCacheUseCase | null,
142
145
  readonly dailySecurityScanUseCase: DailySecurityScanUseCase | null,
143
146
  readonly qualityCheckAdvanceUseCase: QualityCheckAdvanceUseCase,
147
+ readonly reopenedDoneIssueRevertUseCase: ReopenedDoneIssueRevertUseCase,
144
148
  readonly dateRepository: DateRepository,
145
149
  readonly spreadsheetRepository: SpreadsheetRepository,
146
150
  readonly projectRepository: ProjectRepository,
@@ -180,6 +184,7 @@ export class HandleScheduledEventUseCase {
180
184
  awLogStaleThresholdMinutes?: number;
181
185
  awaitingQualityCheckStatus?: string | null;
182
186
  autoAdvanceQualityCheckEnabled?: boolean;
187
+ autoRevertReopenedDoneEnabled?: boolean;
183
188
  labelsAsLlmAgentName?: string[] | null;
184
189
  } | null;
185
190
  thresholdForAutoReject?: number;
@@ -473,6 +478,16 @@ ${JSON.stringify(e)}
473
478
  allowedIssueAuthors,
474
479
  });
475
480
  }
481
+ if (input.startPreparation.autoRevertReopenedDoneEnabled) {
482
+ try {
483
+ await this.reopenedDoneIssueRevertUseCase.run({ project, issues });
484
+ } catch (revertError) {
485
+ console.error(
486
+ `[HandleScheduledEvent] Failed to revert reopened Done issues for project ${project.url}: ${revertError instanceof Error ? revertError.message : String(revertError)}`,
487
+ revertError,
488
+ );
489
+ }
490
+ }
476
491
  if (input.startPreparation.autoAdvanceQualityCheckEnabled) {
477
492
  try {
478
493
  await this.qualityCheckAdvanceUseCase.run({
@@ -489,6 +504,7 @@ ${JSON.stringify(e)}
489
504
  );
490
505
  }
491
506
  }
507
+ await this.issueNoStatusUpdateUseCase.run({ project, issues });
492
508
  const preparationResult = await this.startPreparationUseCase.run({
493
509
  projectUrl: input.projectUrl,
494
510
  defaultAgentName: input.startPreparation.defaultAgentName,
@@ -52,6 +52,7 @@ const issue = (url: string, status: string): Issue => ({
52
52
  author: 'hiromi',
53
53
  closingIssueReferenceUrls: [],
54
54
  agent: null,
55
+ stateReason: null,
55
56
  });
56
57
 
57
58
  describe('InTmuxByHumanSessionTokenCountUseCase', () => {