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
@@ -1,31 +1,81 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resolveNextStepAgentDispatchRepetition = exports.NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD = void 0;
3
+ exports.resolveNextStepAgentDispatchRepetition = exports.DEFAULT_THRESHOLD_FOR_DISPATCH_LOOP = exports.NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD = void 0;
4
4
  const ProjectFieldName_1 = require("../entities/ProjectFieldName");
5
- exports.NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD = 'Next step agent dispatch repeated:';
6
- const resolveNextStepAgentDispatchRepetition = (params) => {
5
+ const extractNextStepAgent_1 = require("./extractNextStepAgent");
6
+ const findLastAgentReport_1 = require("./findLastAgentReport");
7
+ const isAgentReportBody_1 = require("./isAgentReportBody");
8
+ const isHumanComment_1 = require("./isHumanComment");
9
+ const nextStepAgentDispatchRepeatedMessage_1 = require("./nextStepAgentDispatchRepeatedMessage");
10
+ Object.defineProperty(exports, "NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD", { enumerable: true, get: function () { return nextStepAgentDispatchRepeatedMessage_1.NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD; } });
11
+ exports.DEFAULT_THRESHOLD_FOR_DISPATCH_LOOP = 6;
12
+ const countSilentRedispatches = (params) => {
7
13
  if (params.agentFieldValue === null ||
8
14
  (0, ProjectFieldName_1.normalizeProjectFieldName)(params.agentFieldValue) !==
9
15
  (0, ProjectFieldName_1.normalizeProjectFieldName)(params.nextStepAgent)) {
10
- return { type: 'notRepeated' };
16
+ return null;
11
17
  }
12
- const previousRepetitions = params.commentsAfterLastAgentReport.filter((comment) => params.isTrustedAuthor(comment.author) &&
13
- comment.content.startsWith(exports.NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD)).length;
14
- const repetition = previousRepetitions + 1;
15
- if (repetition >= params.thresholdForAutoReject) {
18
+ const lastAgentReport = (0, findLastAgentReport_1.findLastAgentReport)(params.comments, params.isTrustedAuthor);
19
+ const commentsAfterLastAgentReport = lastAgentReport
20
+ ? params.comments.slice(params.comments.indexOf(lastAgentReport) + 1)
21
+ : [];
22
+ return (commentsAfterLastAgentReport.filter((comment) => params.isTrustedAuthor(comment.author) &&
23
+ comment.content.startsWith(nextStepAgentDispatchRepeatedMessage_1.NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD)).length + 1);
24
+ };
25
+ const countDispatchesInCurrentCycle = (params) => {
26
+ const lastHumanCommentIndex = params.comments.reduce((found, comment, index) => (0, isHumanComment_1.isHumanComment)(comment, params.isTrustedAuthor) ? index : found, -1);
27
+ const reportsInCurrentCycle = params.comments
28
+ .slice(lastHumanCommentIndex + 1)
29
+ .filter((comment) => params.isTrustedAuthor(comment.author) &&
30
+ (0, isAgentReportBody_1.isAgentReportBody)(comment.content));
31
+ return (reportsInCurrentCycle.slice(0, -1).filter((comment) => {
32
+ const declared = (0, extractNextStepAgent_1.extractNextStepAgent)(comment.content);
33
+ return (declared !== null &&
34
+ (0, ProjectFieldName_1.normalizeProjectFieldName)(declared) ===
35
+ (0, ProjectFieldName_1.normalizeProjectFieldName)(params.nextStepAgent));
36
+ }).length + 1);
37
+ };
38
+ const resolveNextStepAgentDispatchRepetition = (params) => {
39
+ const silentRedispatches = countSilentRedispatches(params);
40
+ if (silentRedispatches !== null &&
41
+ silentRedispatches >= params.thresholdForAutoReject) {
16
42
  return {
17
43
  type: 'escalateToFailedPreparation',
18
- comment: `${exports.NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD} ${params.nextStepAgent}
44
+ comment: `${nextStepAgentDispatchRepeatedMessage_1.NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD} ${params.nextStepAgent}
19
45
 
20
46
  Failed to receive a report from the dispatched agent for ${params.thresholdForAutoReject} times`,
21
47
  };
22
48
  }
23
- return {
49
+ const dispatchesInCycle = countDispatchesInCurrentCycle(params);
50
+ if (dispatchesInCycle >= params.thresholdForDispatchLoop) {
51
+ return {
52
+ type: 'escalateToFailedPreparation',
53
+ comment: `${nextStepAgentDispatchRepeatedMessage_1.NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD} ${params.nextStepAgent}
54
+
55
+ This agent has been dispatched ${params.thresholdForDispatchLoop} times since the last human comment on this issue and the task has not moved past it, so the issue is escalated for a decision instead of being dispatched again.`,
56
+ };
57
+ }
58
+ const silentRedispatchMessage = {
24
59
  type: 'dispatchAgain',
25
- comment: `${exports.NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD} ${params.nextStepAgent}
60
+ comment: `${nextStepAgentDispatchRepeatedMessage_1.NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD} ${params.nextStepAgent}
26
61
 
27
- The latest agent report names this agent as the next step and the agent field already holds it, so the previous dispatch to it ended without a report. Dispatching it again (${repetition}/${params.thresholdForAutoReject}).`,
62
+ The latest agent report names this agent as the next step and the agent field already holds it, so the previous dispatch to it ended without a report. Dispatching it again (${silentRedispatches}/${params.thresholdForAutoReject}).`,
28
63
  };
64
+ if (silentRedispatches !== null && silentRedispatches > 1) {
65
+ return silentRedispatchMessage;
66
+ }
67
+ if (dispatchesInCycle > 1) {
68
+ return {
69
+ type: 'dispatchAgain',
70
+ comment: `${nextStepAgentDispatchRepeatedMessage_1.NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD} ${params.nextStepAgent}
71
+
72
+ The latest agent report names this agent as the next step and it has already been dispatched on this issue since the last human comment. Dispatching it again (${dispatchesInCycle}/${params.thresholdForDispatchLoop}).`,
73
+ };
74
+ }
75
+ if (silentRedispatches !== null) {
76
+ return silentRedispatchMessage;
77
+ }
78
+ return { type: 'notRepeated' };
29
79
  };
30
80
  exports.resolveNextStepAgentDispatchRepetition = resolveNextStepAgentDispatchRepetition;
31
81
  //# sourceMappingURL=resolveNextStepAgentDispatchRepetition.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"resolveNextStepAgentDispatchRepetition.js","sourceRoot":"","sources":["../../../src/domain/usecases/resolveNextStepAgentDispatchRepetition.ts"],"names":[],"mappings":";;;AAAA,mEAAyE;AAE5D,QAAA,8CAA8C,GACzD,oCAAoC,CAAC;AAOhC,MAAM,sCAAsC,GAAG,CAEpD,MAMD,EAAmC,EAAE;IACpC,IACE,MAAM,CAAC,eAAe,KAAK,IAAI;QAC/B,IAAA,4CAAyB,EAAC,MAAM,CAAC,eAAe,CAAC;YAC/C,IAAA,4CAAyB,EAAC,MAAM,CAAC,aAAa,CAAC,EACjD,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC;IACjC,CAAC;IACD,MAAM,mBAAmB,GAAG,MAAM,CAAC,4BAA4B,CAAC,MAAM,CACpE,CAAC,OAAO,EAAE,EAAE,CACV,MAAM,CAAC,eAAe,CAAC,OAAO,CAAC,MAAM,CAAC;QACtC,OAAO,CAAC,OAAO,CAAC,UAAU,CACxB,sDAA8C,CAC/C,CACJ,CAAC,MAAM,CAAC;IACT,MAAM,UAAU,GAAG,mBAAmB,GAAG,CAAC,CAAC;IAC3C,IAAI,UAAU,IAAI,MAAM,CAAC,sBAAsB,EAAE,CAAC;QAChD,OAAO;YACL,IAAI,EAAE,6BAA6B;YACnC,OAAO,EAAE,GAAG,sDAA8C,IAAI,MAAM,CAAC,aAAa;;2DAE7B,MAAM,CAAC,sBAAsB,QAAQ;SAC3F,CAAC;IACJ,CAAC;IACD,OAAO;QACL,IAAI,EAAE,eAAe;QACrB,OAAO,EAAE,GAAG,sDAA8C,IAAI,MAAM,CAAC,aAAa;;+KAEyF,UAAU,IAAI,MAAM,CAAC,sBAAsB,IAAI;KAC3N,CAAC;AACJ,CAAC,CAAC;AAtCW,QAAA,sCAAsC,0CAsCjD"}
1
+ {"version":3,"file":"resolveNextStepAgentDispatchRepetition.js","sourceRoot":"","sources":["../../../src/domain/usecases/resolveNextStepAgentDispatchRepetition.ts"],"names":[],"mappings":";;;AAAA,mEAAyE;AACzE,iEAA8D;AAC9D,+DAA4D;AAC5D,2DAAwD;AACxD,qDAAkD;AAClD,iGAAwG;AAE/F,+HAFA,qFAA8C,OAEA;AAE1C,QAAA,mCAAmC,GAAG,CAAC,CAAC;AAOrD,MAAM,uBAAuB,GAAG,CAE9B,MAKD,EAAiB,EAAE;IAClB,IACE,MAAM,CAAC,eAAe,KAAK,IAAI;QAC/B,IAAA,4CAAyB,EAAC,MAAM,CAAC,eAAe,CAAC;YAC/C,IAAA,4CAAyB,EAAC,MAAM,CAAC,aAAa,CAAC,EACjD,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,eAAe,GAAG,IAAA,yCAAmB,EACzC,MAAM,CAAC,QAAQ,EACf,MAAM,CAAC,eAAe,CACvB,CAAC;IACF,MAAM,4BAA4B,GAAG,eAAe;QAClD,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;QACrE,CAAC,CAAC,EAAE,CAAC;IACP,OAAO,CACL,4BAA4B,CAAC,MAAM,CACjC,CAAC,OAAO,EAAE,EAAE,CACV,MAAM,CAAC,eAAe,CAAC,OAAO,CAAC,MAAM,CAAC;QACtC,OAAO,CAAC,OAAO,CAAC,UAAU,CACxB,qFAA8C,CAC/C,CACJ,CAAC,MAAM,GAAG,CAAC,CACb,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,6BAA6B,GAAG,CAEpC,MAID,EAAU,EAAE;IACX,MAAM,qBAAqB,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAClD,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,CACxB,IAAA,+BAAc,EAAC,OAAO,EAAE,MAAM,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,EACjE,CAAC,CAAC,CACH,CAAC;IACF,MAAM,qBAAqB,GAAG,MAAM,CAAC,QAAQ;SAC1C,KAAK,CAAC,qBAAqB,GAAG,CAAC,CAAC;SAChC,MAAM,CACL,CAAC,OAAO,EAAE,EAAE,CACV,MAAM,CAAC,eAAe,CAAC,OAAO,CAAC,MAAM,CAAC;QACtC,IAAA,qCAAiB,EAAC,OAAO,CAAC,OAAO,CAAC,CACrC,CAAC;IACJ,OAAO,CACL,qBAAqB,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE;QACpD,MAAM,QAAQ,GAAG,IAAA,2CAAoB,EAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACvD,OAAO,CACL,QAAQ,KAAK,IAAI;YACjB,IAAA,4CAAyB,EAAC,QAAQ,CAAC;gBACjC,IAAA,4CAAyB,EAAC,MAAM,CAAC,aAAa,CAAC,CAClD,CAAC;IACJ,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CACd,CAAC;AACJ,CAAC,CAAC;AAEK,MAAM,sCAAsC,GAAG,CAEpD,MAOD,EAAmC,EAAE;IACpC,MAAM,kBAAkB,GAAG,uBAAuB,CAAC,MAAM,CAAC,CAAC;IAC3D,IACE,kBAAkB,KAAK,IAAI;QAC3B,kBAAkB,IAAI,MAAM,CAAC,sBAAsB,EACnD,CAAC;QACD,OAAO;YACL,IAAI,EAAE,6BAA6B;YACnC,OAAO,EAAE,GAAG,qFAA8C,IAAI,MAAM,CAAC,aAAa;;2DAE7B,MAAM,CAAC,sBAAsB,QAAQ;SAC3F,CAAC;IACJ,CAAC;IACD,MAAM,iBAAiB,GAAG,6BAA6B,CAAC,MAAM,CAAC,CAAC;IAChE,IAAI,iBAAiB,IAAI,MAAM,CAAC,wBAAwB,EAAE,CAAC;QACzD,OAAO;YACL,IAAI,EAAE,6BAA6B;YACnC,OAAO,EAAE,GAAG,qFAA8C,IAAI,MAAM,CAAC,aAAa;;iCAEvD,MAAM,CAAC,wBAAwB,mKAAmK;SAC9N,CAAC;IACJ,CAAC;IACD,MAAM,uBAAuB,GAAG;QAC9B,IAAI,EAAE,eAAwB;QAC9B,OAAO,EAAE,GAAG,qFAA8C,IAAI,MAAM,CAAC,aAAa;;+KAEyF,kBAAkB,IAAI,MAAM,CAAC,sBAAsB,IAAI;KACnO,CAAC;IACF,IAAI,kBAAkB,KAAK,IAAI,IAAI,kBAAkB,GAAG,CAAC,EAAE,CAAC;QAC1D,OAAO,uBAAuB,CAAC;IACjC,CAAC;IACD,IAAI,iBAAiB,GAAG,CAAC,EAAE,CAAC;QAC1B,OAAO;YACL,IAAI,EAAE,eAAe;YACrB,OAAO,EAAE,GAAG,qFAA8C,IAAI,MAAM,CAAC,aAAa;;iKAEyE,iBAAiB,IAAI,MAAM,CAAC,wBAAwB,IAAI;SACpN,CAAC;IACJ,CAAC;IACD,IAAI,kBAAkB,KAAK,IAAI,EAAE,CAAC;QAChC,OAAO,uBAAuB,CAAC;IACjC,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC;AACjC,CAAC,CAAC;AApDW,QAAA,sCAAsC,0CAoDjD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-issue-tower-defence-management",
3
- "version": "1.187.3",
3
+ "version": "1.188.1",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -1510,6 +1510,7 @@ mysteryKey: 'value'
1510
1510
  projectUrl: 'https://github.com/orgs/test/projects/1',
1511
1511
  issueUrl: 'https://github.com/test/repo/issues/1',
1512
1512
  thresholdForAutoReject: 3,
1513
+ thresholdForDispatchLoop: 6,
1513
1514
  workflowBlockerResolvedWebhookUrl: null,
1514
1515
  allowedIssueAuthors: null,
1515
1516
  labelsAsLlmAgentName: null,
@@ -1553,6 +1554,7 @@ mysteryKey: 'value'
1553
1554
  projectUrl: 'https://github.com/orgs/override/projects/2',
1554
1555
  issueUrl: 'https://github.com/test/repo/issues/1',
1555
1556
  thresholdForAutoReject: 3,
1557
+ thresholdForDispatchLoop: 6,
1556
1558
  workflowBlockerResolvedWebhookUrl: null,
1557
1559
  allowedIssueAuthors: null,
1558
1560
  labelsAsLlmAgentName: null,
@@ -33,6 +33,7 @@ import { projectCacheDirectory } from '../../repositories/localStorageCacheDirec
33
33
  import { NodeLocalCommandRunner } from '../../repositories/NodeLocalCommandRunner';
34
34
  import { NodeTmuxSessionRepository } from '../../repositories/NodeTmuxSessionRepository';
35
35
  import { ProcTakeOwnershipSpawnRepository } from '../../repositories/ProcTakeOwnershipSpawnRepository';
36
+ import { DEFAULT_THRESHOLD_FOR_DISPATCH_LOOP } from '../../../domain/usecases/resolveNextStepAgentDispatchRepetition';
36
37
  import { ProxyClaudeTokenUsageRepository } from '../../repositories/ProxyClaudeTokenUsageRepository';
37
38
  import { SystemDateRepository } from '../../repositories/SystemDateRepository';
38
39
  import {
@@ -89,10 +90,29 @@ type StartDaemonOptions = {
89
90
  configFilePath: string;
90
91
  };
91
92
 
93
+ const resolvePositiveIntegerOption = (
94
+ rawValue: number | string | undefined,
95
+ optionName: string,
96
+ fallback: number,
97
+ ): number => {
98
+ if (rawValue === undefined) {
99
+ return fallback;
100
+ }
101
+ const parsed = Number(rawValue);
102
+ if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed <= 0) {
103
+ console.error(
104
+ `Invalid value for --${optionName}. It must be a positive integer.`,
105
+ );
106
+ process.exit(1);
107
+ }
108
+ return parsed;
109
+ };
110
+
92
111
  type NotifyFinishedOptions = {
93
112
  issueUrl: string;
94
113
  projectUrl?: string;
95
114
  thresholdForAutoReject?: string;
115
+ thresholdForDispatchLoop?: string;
96
116
  workflowBlockerResolvedWebhookUrl?: string;
97
117
  configFilePath: string;
98
118
  missingAgentName?: string;
@@ -414,6 +434,7 @@ program
414
434
  projectUrl,
415
435
  preparationProcessCheckCommand,
416
436
  thresholdForAutoReject: config.thresholdForAutoReject ?? 3,
437
+ thresholdForDispatchLoop: config.thresholdForDispatchLoop,
417
438
  awLogDirectoryPath: config.awLogDirectoryPath,
418
439
  awLogStaleThresholdMinutes: config.awLogStaleThresholdMinutes,
419
440
  labelsAsLlmAgentName: config.labelsAsLlmAgentName ?? null,
@@ -498,6 +519,10 @@ program
498
519
  '--thresholdForAutoReject <count>',
499
520
  'Threshold for auto-escalation after consecutive rejections (default: 3)',
500
521
  )
522
+ .option(
523
+ '--thresholdForDispatchLoop <count>',
524
+ 'Threshold for auto-escalation after one agent is dispatched this many times since the last human comment, whether it names itself or two agents name each other in turn (default: 6)',
525
+ )
501
526
  .option(
502
527
  '--workflowBlockerResolvedWebhookUrl <url>',
503
528
  'Webhook URL to notify when a workflow blocker issue status changes to awaiting quality check. Supports {URL} and {MESSAGE} placeholders.',
@@ -528,6 +553,9 @@ program
528
553
  thresholdForAutoReject: options.thresholdForAutoReject
529
554
  ? Number(options.thresholdForAutoReject)
530
555
  : undefined,
556
+ thresholdForDispatchLoop: options.thresholdForDispatchLoop
557
+ ? Number(options.thresholdForDispatchLoop)
558
+ : undefined,
531
559
  workflowBlockerResolvedWebhookUrl:
532
560
  options.workflowBlockerResolvedWebhookUrl,
533
561
  };
@@ -558,22 +586,16 @@ program
558
586
  process.exit(1);
559
587
  }
560
588
 
561
- let thresholdForAutoReject = 3;
562
- const rawThreshold = config.thresholdForAutoReject;
563
- if (rawThreshold !== undefined) {
564
- const parsed = Number(rawThreshold);
565
- if (
566
- !Number.isFinite(parsed) ||
567
- !Number.isInteger(parsed) ||
568
- parsed <= 0
569
- ) {
570
- console.error(
571
- 'Invalid value for --thresholdForAutoReject. It must be a positive integer.',
572
- );
573
- process.exit(1);
574
- }
575
- thresholdForAutoReject = parsed;
576
- }
589
+ const thresholdForAutoReject = resolvePositiveIntegerOption(
590
+ config.thresholdForAutoReject,
591
+ 'thresholdForAutoReject',
592
+ 3,
593
+ );
594
+ const thresholdForDispatchLoop = resolvePositiveIntegerOption(
595
+ config.thresholdForDispatchLoop,
596
+ 'thresholdForDispatchLoop',
597
+ DEFAULT_THRESHOLD_FOR_DISPATCH_LOOP,
598
+ );
577
599
 
578
600
  const workflowBlockerResolvedWebhookUrl: string | null =
579
601
  config.workflowBlockerResolvedWebhookUrl ?? null;
@@ -640,6 +662,7 @@ program
640
662
  projectUrl,
641
663
  issueUrl: options.issueUrl,
642
664
  thresholdForAutoReject,
665
+ thresholdForDispatchLoop,
643
666
  workflowBlockerResolvedWebhookUrl,
644
667
  allowedIssueAuthors,
645
668
  labelsAsLlmAgentName: config.labelsAsLlmAgentName ?? null,
@@ -15,6 +15,7 @@ export type ConfigFile = {
15
15
  allowedIssueAuthors?: string;
16
16
  autoAssignManagerAuthors?: string;
17
17
  thresholdForAutoReject?: number;
18
+ thresholdForDispatchLoop?: number;
18
19
  workflowBlockerResolvedWebhookUrl?: string;
19
20
  projectName?: string;
20
21
  preparationProcessCheckCommand?: string;
@@ -1,17 +1,21 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite';
2
- import { useState } from 'react';
2
+ import { type ComponentProps, useEffect, useRef, useState } from 'react';
3
3
  import type { ConsoleTabName } from '../../logic/types';
4
4
  import { ConsoleTabList } from './ConsoleTabList';
5
5
 
6
+ const allPjcodes = ['utage3', 'cmg', 'xmile', 'xcare', 'umino'];
7
+
6
8
  const meta: Meta<typeof ConsoleTabList> = {
7
9
  title: 'Console/ConsoleTabList',
8
10
  component: ConsoleTabList,
9
11
  args: {
10
12
  pjcode: 'acme',
13
+ pjcodes: allPjcodes,
11
14
  generatedAt: '2026-06-19T08:42:11.000Z',
12
15
  fromCache: false,
13
16
  tabHref: (tab: ConsoleTabName) => `/projects/acme/${tab}`,
14
17
  onSelectTab: () => {},
18
+ onSelectProject: () => {},
15
19
  },
16
20
  };
17
21
 
@@ -126,3 +130,37 @@ export const WithSettingsButton: Story = {
126
130
  ),
127
131
  },
128
132
  };
133
+
134
+ export const WithProjectSwitcherClosed: Story = {
135
+ args: {
136
+ activeTab: 'prs',
137
+ counts,
138
+ pjcode: 'umino',
139
+ pjcodes: allPjcodes,
140
+ },
141
+ };
142
+
143
+ const DropdownOpen = (args: ComponentProps<typeof ConsoleTabList>) => {
144
+ const containerRef = useRef<HTMLDivElement>(null);
145
+ useEffect(() => {
146
+ const btn = containerRef.current?.querySelector<HTMLButtonElement>(
147
+ '.console-tab-pjname-button',
148
+ );
149
+ btn?.click();
150
+ }, []);
151
+ return (
152
+ <div ref={containerRef}>
153
+ <ConsoleTabList {...args} />
154
+ </div>
155
+ );
156
+ };
157
+
158
+ export const WithProjectSwitcherOpen: Story = {
159
+ args: {
160
+ activeTab: 'prs',
161
+ counts,
162
+ pjcode: 'umino',
163
+ pjcodes: allPjcodes,
164
+ },
165
+ render: (args) => <DropdownOpen {...args} />,
166
+ };
@@ -14,10 +14,12 @@ const counts: Record<ConsoleTabName, number> = {
14
14
 
15
15
  const baseProps = {
16
16
  pjcode: 'acme',
17
+ pjcodes: ['utage3', 'cmg', 'xmile', 'xcare', 'umino'],
17
18
  generatedAt: '2026-06-19T08:42:11.000Z',
18
19
  fromCache: false,
19
20
  tabHref: (tab: ConsoleTabName) => `/projects/acme/${tab}`,
20
21
  onSelectTab: () => {},
22
+ onSelectProject: () => {},
21
23
  };
22
24
 
23
25
  describe('ConsoleTabList', () => {
@@ -170,4 +172,63 @@ describe('ConsoleTabList', () => {
170
172
  );
171
173
  expect(container.querySelector('.console-tab-settings')).toBeNull();
172
174
  });
175
+
176
+ it('opens the project dropdown when the pjcode button is clicked', () => {
177
+ const { getByRole, baseElement } = render(
178
+ <ConsoleTabList {...baseProps} activeTab="prs" counts={counts} />,
179
+ );
180
+ expect(
181
+ baseElement.querySelector('.console-tab-pjname-dropdown'),
182
+ ).toBeNull();
183
+ fireEvent.click(getByRole('button', { name: /acme/i }));
184
+ expect(
185
+ baseElement.querySelector('.console-tab-pjname-dropdown'),
186
+ ).toBeInTheDocument();
187
+ });
188
+
189
+ it('lists all pjcodes in the dropdown', () => {
190
+ const { getByRole, baseElement } = render(
191
+ <ConsoleTabList {...baseProps} activeTab="prs" counts={counts} />,
192
+ );
193
+ fireEvent.click(getByRole('button', { name: /acme/i }));
194
+ const options = Array.from(
195
+ baseElement.querySelectorAll('.console-tab-pjname-option'),
196
+ );
197
+ expect(options.map((o) => o.textContent)).toEqual([
198
+ 'utage3',
199
+ 'cmg',
200
+ 'xmile',
201
+ 'xcare',
202
+ 'umino',
203
+ ]);
204
+ });
205
+
206
+ it('calls onSelectProject with the chosen pjcode when a dropdown option is clicked', () => {
207
+ const onSelectProject = jest.fn();
208
+ const { getByRole, getByText } = render(
209
+ <ConsoleTabList
210
+ {...baseProps}
211
+ activeTab="prs"
212
+ counts={counts}
213
+ onSelectProject={onSelectProject}
214
+ />,
215
+ );
216
+ fireEvent.click(getByRole('button', { name: /acme/i }));
217
+ fireEvent.click(getByText('xmile'));
218
+ expect(onSelectProject).toHaveBeenCalledWith('xmile');
219
+ });
220
+
221
+ it('closes the dropdown after a project option is selected', () => {
222
+ const { getByRole, getByText, baseElement } = render(
223
+ <ConsoleTabList {...baseProps} activeTab="prs" counts={counts} />,
224
+ );
225
+ fireEvent.click(getByRole('button', { name: /acme/i }));
226
+ expect(
227
+ baseElement.querySelector('.console-tab-pjname-dropdown'),
228
+ ).toBeInTheDocument();
229
+ fireEvent.click(getByText('xmile'));
230
+ expect(
231
+ baseElement.querySelector('.console-tab-pjname-dropdown'),
232
+ ).toBeNull();
233
+ });
173
234
  });
@@ -1,14 +1,17 @@
1
- import type { ReactNode } from 'react';
1
+ import { type ReactNode, useEffect, useRef, useState } from 'react';
2
+ import { createPortal } from 'react-dom';
2
3
  import { CONSOLE_TABS, type ConsoleTabName } from '../../logic/types';
3
4
 
4
5
  export type ConsoleTabBarProps = {
5
6
  activeTab: ConsoleTabName;
6
7
  counts: Record<ConsoleTabName, number>;
7
8
  pjcode: string | null;
9
+ pjcodes: string[];
8
10
  generatedAt: string | null;
9
11
  fromCache: boolean;
10
12
  tabHref: (tab: ConsoleTabName) => string;
11
13
  onSelectTab: (tab: ConsoleTabName) => void;
14
+ onSelectProject: (pjcode: string) => void;
12
15
  settingsButton?: ReactNode;
13
16
  };
14
17
 
@@ -16,12 +19,51 @@ export const ConsoleTabList = ({
16
19
  activeTab,
17
20
  counts,
18
21
  pjcode,
22
+ pjcodes,
19
23
  generatedAt,
20
24
  fromCache,
21
25
  tabHref,
22
26
  onSelectTab,
27
+ onSelectProject,
23
28
  settingsButton,
24
29
  }: ConsoleTabBarProps) => {
30
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
31
+ const [dropdownPos, setDropdownPos] = useState<{
32
+ top: number;
33
+ right: number;
34
+ } | null>(null);
35
+ const buttonRef = useRef<HTMLButtonElement>(null);
36
+ const dropdownRef = useRef<HTMLDivElement>(null);
37
+
38
+ useEffect(() => {
39
+ if (!isDropdownOpen) return;
40
+ const handleClickOutside = (event: MouseEvent) => {
41
+ if (
42
+ dropdownRef.current &&
43
+ !dropdownRef.current.contains(event.target as Node) &&
44
+ buttonRef.current &&
45
+ !buttonRef.current.contains(event.target as Node)
46
+ ) {
47
+ setIsDropdownOpen(false);
48
+ }
49
+ };
50
+ document.addEventListener('mousedown', handleClickOutside);
51
+ return () => {
52
+ document.removeEventListener('mousedown', handleClickOutside);
53
+ };
54
+ }, [isDropdownOpen]);
55
+
56
+ const handleButtonClick = () => {
57
+ if (!isDropdownOpen && buttonRef.current) {
58
+ const rect = buttonRef.current.getBoundingClientRect();
59
+ setDropdownPos({
60
+ top: rect.bottom + 4,
61
+ right: window.innerWidth - rect.right,
62
+ });
63
+ }
64
+ setIsDropdownOpen((prev) => !prev);
65
+ };
66
+
25
67
  return (
26
68
  <nav aria-label="Console tabs" className="console-tabbar">
27
69
  {CONSOLE_TABS.filter((tab) => {
@@ -62,7 +104,55 @@ export const ConsoleTabList = ({
62
104
  </a>
63
105
  );
64
106
  })}
65
- {pjcode !== null && <span className="console-tab-pjname">{pjcode}</span>}
107
+ {pjcode !== null && (
108
+ <div className="console-tab-pjname">
109
+ <button
110
+ ref={buttonRef}
111
+ type="button"
112
+ className="console-tab-pjname-button"
113
+ aria-expanded={isDropdownOpen}
114
+ aria-haspopup="menu"
115
+ onClick={handleButtonClick}
116
+ >
117
+ {pjcode}
118
+ <span className="console-tab-pjname-arrow" aria-hidden="true">
119
+ ▾
120
+ </span>
121
+ </button>
122
+ {isDropdownOpen &&
123
+ dropdownPos !== null &&
124
+ createPortal(
125
+ <div
126
+ ref={dropdownRef}
127
+ role="menu"
128
+ className="console-tab-pjname-dropdown"
129
+ aria-label="Select project"
130
+ style={{
131
+ position: 'fixed',
132
+ top: dropdownPos.top,
133
+ right: dropdownPos.right,
134
+ }}
135
+ >
136
+ {pjcodes.map((code) => (
137
+ <button
138
+ key={code}
139
+ type="button"
140
+ role="menuitem"
141
+ className="console-tab-pjname-option"
142
+ data-active={code === pjcode ? 'true' : undefined}
143
+ onClick={() => {
144
+ onSelectProject(code);
145
+ setIsDropdownOpen(false);
146
+ }}
147
+ >
148
+ {code}
149
+ </button>
150
+ ))}
151
+ </div>,
152
+ document.body,
153
+ )}
154
+ </div>
155
+ )}
66
156
  {generatedAt !== null && (
67
157
  <span
68
158
  className="console-tab-geninfo"
@@ -476,10 +476,12 @@ export const ConsolePage = () => {
476
476
  activeTab={activeTab}
477
477
  counts={counts}
478
478
  pjcode={pjcode}
479
+ pjcodes={pjcodes}
479
480
  generatedAt={generatedAt}
480
481
  fromCache={fromCache}
481
482
  tabHref={navigation.tabHref}
482
483
  onSelectTab={navigation.selectTab}
484
+ onSelectProject={(code) => navigateAssign(`/projects/${code}`)}
483
485
  settingsButton={
484
486
  <ConsoleTimerSettingsModalDialog
485
487
  isOpen={isSettingsOpen}
@@ -103,8 +103,58 @@ body {
103
103
  margin-left: auto;
104
104
  align-self: center;
105
105
  padding: 0 8px;
106
+ }
107
+
108
+ .console-tab-pjname-button {
109
+ background: transparent;
110
+ border: none;
106
111
  color: #8b949e;
112
+ cursor: pointer;
107
113
  font-size: 0.7188rem;
114
+ padding: 2px 4px;
115
+ border-radius: 4px;
116
+ display: flex;
117
+ align-items: center;
118
+ gap: 3px;
119
+ line-height: 1;
120
+ }
121
+
122
+ .console-tab-pjname-button:hover {
123
+ color: #e6edf3;
124
+ background: #21262d;
125
+ }
126
+
127
+ .console-tab-pjname-arrow {
128
+ font-size: 0.625rem;
129
+ }
130
+
131
+ .console-tab-pjname-dropdown {
132
+ background: #161b22;
133
+ border: 1px solid #30363d;
134
+ border-radius: 6px;
135
+ list-style: none;
136
+ margin: 0;
137
+ padding: 4px 0;
138
+ min-width: 120px;
139
+ z-index: 100;
140
+ box-shadow: 0 8px 24px rgba(1, 4, 9, 0.5);
141
+ }
142
+
143
+ .console-tab-pjname-option {
144
+ display: block;
145
+ width: 100%;
146
+ background: transparent;
147
+ border: none;
148
+ color: #e6edf3;
149
+ cursor: pointer;
150
+ font-size: 0.7188rem;
151
+ padding: 4px 12px;
152
+ text-align: left;
153
+ }
154
+
155
+ .console-tab-pjname-option:hover,
156
+ .console-tab-pjname-option[data-active="true"] {
157
+ background: #21262d;
108
158
  }
109
159
 
110
160
  .console-tab-geninfo {