issue-flow 0.11.1 → 0.13.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 (180) hide show
  1. package/README.md +79 -274
  2. package/dist/agent-BXRIEKVM.js +238 -0
  3. package/dist/agent-BXRIEKVM.js.map +1 -0
  4. package/dist/{analyze-XV3K25K3.js → analyze-WS47UYGN.js} +32 -15
  5. package/dist/analyze-WS47UYGN.js.map +1 -0
  6. package/dist/{apply-WUJHDD7A.js → apply-OS62UWRN.js} +15 -5
  7. package/dist/{apply-WUJHDD7A.js.map → apply-OS62UWRN.js.map} +1 -1
  8. package/dist/bench-SA3BOME5.js +906 -0
  9. package/dist/bench-SA3BOME5.js.map +1 -0
  10. package/dist/chunk-37N2RJW2.js +120 -0
  11. package/dist/chunk-37N2RJW2.js.map +1 -0
  12. package/dist/chunk-3XLCNVMD.js +180 -0
  13. package/dist/chunk-3XLCNVMD.js.map +1 -0
  14. package/dist/chunk-5TB6KLC2.js +72 -0
  15. package/dist/chunk-5TB6KLC2.js.map +1 -0
  16. package/dist/{chunk-OBLVLIUS.js → chunk-664Y3NDX.js} +25 -13
  17. package/dist/chunk-664Y3NDX.js.map +1 -0
  18. package/dist/chunk-6FDFRNXZ.js +357 -0
  19. package/dist/chunk-6FDFRNXZ.js.map +1 -0
  20. package/dist/chunk-7KD66LIM.js +658 -0
  21. package/dist/chunk-7KD66LIM.js.map +1 -0
  22. package/dist/chunk-7Z4Y3VKM.js +26 -0
  23. package/dist/chunk-7Z4Y3VKM.js.map +1 -0
  24. package/dist/chunk-ARFVHC2F.js +1111 -0
  25. package/dist/chunk-ARFVHC2F.js.map +1 -0
  26. package/dist/{chunk-RKTEV6EG.js → chunk-BHJCGWQX.js} +141 -12
  27. package/dist/chunk-BHJCGWQX.js.map +1 -0
  28. package/dist/chunk-BQDQTIM7.js +74 -0
  29. package/dist/chunk-BQDQTIM7.js.map +1 -0
  30. package/dist/chunk-CNBYCMSU.js +2709 -0
  31. package/dist/chunk-CNBYCMSU.js.map +1 -0
  32. package/dist/chunk-CRGIQFUQ.js +83 -0
  33. package/dist/chunk-CRGIQFUQ.js.map +1 -0
  34. package/dist/chunk-DIG5XKQQ.js +298 -0
  35. package/dist/chunk-DIG5XKQQ.js.map +1 -0
  36. package/dist/chunk-FZDR7CGH.js +71 -0
  37. package/dist/chunk-FZDR7CGH.js.map +1 -0
  38. package/dist/chunk-HEF2RMKN.js +413 -0
  39. package/dist/chunk-HEF2RMKN.js.map +1 -0
  40. package/dist/{chunk-IVHZEYAX.js → chunk-HJGGNLWI.js} +25 -19
  41. package/dist/chunk-HJGGNLWI.js.map +1 -0
  42. package/dist/chunk-HQAHNIAR.js +283 -0
  43. package/dist/chunk-HQAHNIAR.js.map +1 -0
  44. package/dist/{chunk-GANSDJV7.js → chunk-IHIEXUTC.js} +46 -24
  45. package/dist/chunk-IHIEXUTC.js.map +1 -0
  46. package/dist/chunk-IV5SS4JN.js +84 -0
  47. package/dist/chunk-IV5SS4JN.js.map +1 -0
  48. package/dist/{chunk-HC2MT7BX.js → chunk-IYV6KNVF.js} +158 -592
  49. package/dist/chunk-IYV6KNVF.js.map +1 -0
  50. package/dist/{chunk-LVKQ2AKT.js → chunk-J62QSVP6.js} +10 -8
  51. package/dist/chunk-J62QSVP6.js.map +1 -0
  52. package/dist/chunk-KMMASOA6.js +134 -0
  53. package/dist/chunk-KMMASOA6.js.map +1 -0
  54. package/dist/chunk-KQHB7ZT3.js +559 -0
  55. package/dist/chunk-KQHB7ZT3.js.map +1 -0
  56. package/dist/chunk-L6HSKTQE.js +77 -0
  57. package/dist/chunk-L6HSKTQE.js.map +1 -0
  58. package/dist/{chunk-6N7LZZ5U.js → chunk-LZ6GFQAG.js} +474 -160
  59. package/dist/chunk-LZ6GFQAG.js.map +1 -0
  60. package/dist/{chunk-HQYXVIH6.js → chunk-MIVHK5YI.js} +5 -5
  61. package/dist/chunk-NEZZ6KDR.js +1217 -0
  62. package/dist/chunk-NEZZ6KDR.js.map +1 -0
  63. package/dist/{run-DBF2VWUN.js → chunk-OPNGPUKJ.js} +747 -594
  64. package/dist/chunk-OPNGPUKJ.js.map +1 -0
  65. package/dist/chunk-P3LRUNC6.js +16 -0
  66. package/dist/chunk-P3LRUNC6.js.map +1 -0
  67. package/dist/{chunk-BXCPXRBR.js → chunk-PIVGJM6K.js} +129 -32
  68. package/dist/chunk-PIVGJM6K.js.map +1 -0
  69. package/dist/{chunk-AAY6KM7N.js → chunk-PV4OX5TC.js} +167 -196
  70. package/dist/chunk-PV4OX5TC.js.map +1 -0
  71. package/dist/chunk-QCXPNSEL.js +141 -0
  72. package/dist/chunk-QCXPNSEL.js.map +1 -0
  73. package/dist/{chunk-34WNTK3K.js → chunk-QXDNBYQF.js} +53 -13
  74. package/dist/chunk-QXDNBYQF.js.map +1 -0
  75. package/dist/chunk-SNTTOQAD.js +30 -0
  76. package/dist/chunk-SNTTOQAD.js.map +1 -0
  77. package/dist/{chunk-EY6WMHV2.js → chunk-TM67EHSV.js} +67 -23
  78. package/dist/chunk-TM67EHSV.js.map +1 -0
  79. package/dist/chunk-TVN2JVOF.js +77 -0
  80. package/dist/chunk-TVN2JVOF.js.map +1 -0
  81. package/dist/chunk-TX5WIVYP.js +129 -0
  82. package/dist/chunk-TX5WIVYP.js.map +1 -0
  83. package/dist/{chunk-KQ4JQN7T.js → chunk-UJ7XWK7J.js} +83 -21
  84. package/dist/chunk-UJ7XWK7J.js.map +1 -0
  85. package/dist/{chunk-47VIN33V.js → chunk-UOCPC2Y2.js} +596 -17
  86. package/dist/chunk-UOCPC2Y2.js.map +1 -0
  87. package/dist/chunk-XEYH6ZZI.js +289 -0
  88. package/dist/chunk-XEYH6ZZI.js.map +1 -0
  89. package/dist/cli.js +390 -40
  90. package/dist/cli.js.map +1 -1
  91. package/dist/config-MEM2IWWN.js +76 -0
  92. package/dist/contract-JH2WBIO6.js +8 -0
  93. package/dist/conventions-WV4NDXGH.js +143 -0
  94. package/dist/conventions-WV4NDXGH.js.map +1 -0
  95. package/dist/execute-NITIHHCK.js +31 -0
  96. package/dist/{generate-TBVTLMOP.js → generate-MVQI7A45.js} +39 -19
  97. package/dist/{generate-TBVTLMOP.js.map → generate-MVQI7A45.js.map} +1 -1
  98. package/dist/git-YRVRUN3C.js +31 -0
  99. package/dist/init-OKWEC3EV.js +21 -0
  100. package/dist/operations-LU6JA3BX.js +374 -0
  101. package/dist/operations-LU6JA3BX.js.map +1 -0
  102. package/dist/permissions-7NMDFU27.js +18 -0
  103. package/dist/plan-LEJJF3DE.js +31 -0
  104. package/dist/{policy-XQW5NUIE.js → policy-JSYEK5B2.js} +17 -4
  105. package/dist/policy-JSYEK5B2.js.map +1 -0
  106. package/dist/pr-HPVUZ7S3.js +36 -0
  107. package/dist/pr-review-R2QZBT3F.js +29 -0
  108. package/dist/pr-review-R2QZBT3F.js.map +1 -0
  109. package/dist/prd-5O6GYCC6.js +30 -0
  110. package/dist/prd-5O6GYCC6.js.map +1 -0
  111. package/dist/ps-TNE44R5V.js +90 -0
  112. package/dist/ps-TNE44R5V.js.map +1 -0
  113. package/dist/recorder-XGF55WXI.js +31 -0
  114. package/dist/recorder-XGF55WXI.js.map +1 -0
  115. package/dist/registry-LGPE53QD.js +15 -0
  116. package/dist/registry-LGPE53QD.js.map +1 -0
  117. package/dist/resume-QMEPMSZW.js +238 -0
  118. package/dist/resume-QMEPMSZW.js.map +1 -0
  119. package/dist/review-YKWD6SNN.js +32 -0
  120. package/dist/review-YKWD6SNN.js.map +1 -0
  121. package/dist/routing-2ZUMZPJB.js +91 -0
  122. package/dist/routing-2ZUMZPJB.js.map +1 -0
  123. package/dist/run-YU4TR62R.js +50 -0
  124. package/dist/run-YU4TR62R.js.map +1 -0
  125. package/dist/state-manager-X3PT3FGT.js +36 -0
  126. package/dist/state-manager-X3PT3FGT.js.map +1 -0
  127. package/dist/usage-VCFLDA5Y.js +174 -0
  128. package/dist/usage-VCFLDA5Y.js.map +1 -0
  129. package/dist/{web-ANQQTTRD.js → web-XVHLX62B.js} +43 -27
  130. package/dist/web-XVHLX62B.js.map +1 -0
  131. package/package.json +6 -2
  132. package/prompts/execute.md +7 -8
  133. package/prompts/plan.md +4 -5
  134. package/prompts/pr.md +13 -6
  135. package/web/AGENTS.md +3 -3
  136. package/web/public/app.css +38 -0
  137. package/web/public/app.js +132 -10
  138. package/web/public/index.html +24 -0
  139. package/dist/analyze-XV3K25K3.js.map +0 -1
  140. package/dist/chunk-34WNTK3K.js.map +0 -1
  141. package/dist/chunk-3BOHTH4V.js +0 -58
  142. package/dist/chunk-3BOHTH4V.js.map +0 -1
  143. package/dist/chunk-47VIN33V.js.map +0 -1
  144. package/dist/chunk-6N7LZZ5U.js.map +0 -1
  145. package/dist/chunk-AAY6KM7N.js.map +0 -1
  146. package/dist/chunk-BXCPXRBR.js.map +0 -1
  147. package/dist/chunk-EWDJENFW.js +0 -303
  148. package/dist/chunk-EWDJENFW.js.map +0 -1
  149. package/dist/chunk-EY6WMHV2.js.map +0 -1
  150. package/dist/chunk-GANSDJV7.js.map +0 -1
  151. package/dist/chunk-HC2MT7BX.js.map +0 -1
  152. package/dist/chunk-HDV3YDTG.js +0 -52
  153. package/dist/chunk-HDV3YDTG.js.map +0 -1
  154. package/dist/chunk-IVHZEYAX.js.map +0 -1
  155. package/dist/chunk-KQ4JQN7T.js.map +0 -1
  156. package/dist/chunk-LVKQ2AKT.js.map +0 -1
  157. package/dist/chunk-OBLVLIUS.js.map +0 -1
  158. package/dist/chunk-RKTEV6EG.js.map +0 -1
  159. package/dist/chunk-VQ5VN7DF.js +0 -1018
  160. package/dist/chunk-VQ5VN7DF.js.map +0 -1
  161. package/dist/config-4THHOXNN.js +0 -41
  162. package/dist/execute-5OEKLM5X.js +0 -15
  163. package/dist/init-PRMOTQ7N.js +0 -9
  164. package/dist/plan-Y2BYZD24.js +0 -17
  165. package/dist/policy-XQW5NUIE.js.map +0 -1
  166. package/dist/pr-H6GLOSXF.js +0 -21
  167. package/dist/pr-review-CK5FR6WX.js +0 -17
  168. package/dist/prd-4PWLCGH5.js +0 -17
  169. package/dist/review-QGVAHEZF.js +0 -15
  170. package/dist/run-DBF2VWUN.js.map +0 -1
  171. package/dist/web-ANQQTTRD.js.map +0 -1
  172. /package/dist/{chunk-HQYXVIH6.js.map → chunk-MIVHK5YI.js.map} +0 -0
  173. /package/dist/{config-4THHOXNN.js.map → config-MEM2IWWN.js.map} +0 -0
  174. /package/dist/{execute-5OEKLM5X.js.map → contract-JH2WBIO6.js.map} +0 -0
  175. /package/dist/{init-PRMOTQ7N.js.map → execute-NITIHHCK.js.map} +0 -0
  176. /package/dist/{plan-Y2BYZD24.js.map → git-YRVRUN3C.js.map} +0 -0
  177. /package/dist/{pr-H6GLOSXF.js.map → init-OKWEC3EV.js.map} +0 -0
  178. /package/dist/{pr-review-CK5FR6WX.js.map → permissions-7NMDFU27.js.map} +0 -0
  179. /package/dist/{prd-4PWLCGH5.js.map → plan-LEJJF3DE.js.map} +0 -0
  180. /package/dist/{review-QGVAHEZF.js.map → pr-HPVUZ7S3.js.map} +0 -0
package/prompts/pr.md CHANGED
@@ -14,13 +14,20 @@ __ISSUE_BODY__
14
14
  </issue-body>
15
15
 
16
16
  Steps:
17
- 1. Read the task plan from __TASKS_PATH__ if it exists
18
- 2. Review the git log for this branch: git log __BASE_BRANCH__..HEAD --oneline
19
- 3. Review the diff: git diff __BASE_BRANCH__...HEAD --stat
20
- 4. Create a well-structured PR using gh pr create
17
+ 1. Check whether this branch already has an open Pull Request:
18
+ `gh pr list --head __BRANCH_NAME__ --state open --json number,url`.
19
+ If it does, do NOT create a second one — report the existing URL and stop.
20
+ The pipeline checks this before invoking you and normally skips this phase
21
+ entirely, so reaching this point with a Pull Request already open means the
22
+ check could not run: it is the last guard against a duplicate, not a
23
+ formality.
24
+ 2. Read the task plan from __TASKS_PATH__ if it exists
25
+ 3. Review the git log for this branch: git log __BASE_BRANCH__..HEAD --oneline
26
+ 4. Review the diff: git diff __BASE_BRANCH__...HEAD --stat
27
+ 5. Create a well-structured PR using gh pr create
21
28
 
22
29
  The PR should:
23
- - Have a clear, concise title (under 70 characters)
30
+ - Use this title verbatim: `__PR_TITLE_CONVENTION__`
24
31
  - Include a summary of changes
25
32
  - Include a test plan
26
33
  - Cite where the demand is described: the reference above (`__ISSUE_URL__`)
@@ -29,7 +36,7 @@ Issue reference line(s):
29
36
  - Include the lines below verbatim in the PR body, each entry on its own line,
30
37
  with no surrounding code fence or quoting — GitHub only auto-closes an issue
31
38
  when the `Closes #N` line is plain body text:
32
- __ISSUE_CLOSES__
39
+ __ISSUE_REFERENCE__
33
40
  - When nothing follows the line above, the issue has no GitHub counterpart: do
34
41
  NOT invent a "Closes #" reference, and cite `__ISSUE_URL__` in the body instead
35
42
 
package/web/AGENTS.md CHANGED
@@ -10,7 +10,7 @@ vitest. Toda verificação de mudança aqui é manual, no navegador.
10
10
 
11
11
  O painel consome `GET api/sessions` (lista enriquecida para o dashboard),
12
12
  `GET api/status` (o `SessionSnapshot` serializado, opcionalmente com
13
- `?session=<id>`) e `GET api/health`. Ele precisa renderizar **session.json de
13
+ `?session=<id>`), `GET api/events?session=<id>` (journal) e `GET api/health`. Ele precisa renderizar **session.json de
14
14
  execuções antigas**, então todo campo pode chegar como `undefined` (não
15
15
  existia na versão que gravou o arquivo) além do `null` (existe, não
16
16
  informado). Os dois significam "não informado" e nunca podem virar `0`,
@@ -36,7 +36,7 @@ do snapshot.
36
36
 
37
37
  ## Abas, Kanban e drawer
38
38
 
39
- O painel tem duas abas ("Execução" e "Kanban") e um drawer de detalhes por
39
+ O painel tem três abas ("Execução", "Kanban" e "Histórico") e um drawer de detalhes por
40
40
  story. Três regras seguram esse conjunto:
41
41
 
42
42
  - **Acesso a story sempre por `getStoryById()` / `getStories()`.** Elas são a
@@ -51,7 +51,7 @@ story. Três regras seguram esse conjunto:
51
51
  então uma referência guardada na abertura apontaria para um nó fora do
52
52
  documento. Pelo mesmo motivo o foco volta ao card via
53
53
  `[data-story-id="…"]` no momento de fechar.
54
- - **`renderKanban()` e `renderDrawer()` são chamadas incondicionalmente** dentro
54
+ - **`renderKanban()`, `renderHistory()` e `renderDrawer()` são chamadas incondicionalmente** dentro
55
55
  de `render()`, não ao trocar de aba. Uma aba inativa não pode ficar defasada,
56
56
  e é o `renderDrawer()` de cada poll que mantém o drawer aberto em dia (e o
57
57
  fecha quando a story some do plano).
@@ -47,6 +47,10 @@
47
47
  box-sizing: border-box;
48
48
  }
49
49
 
50
+ [hidden] {
51
+ display: none !important;
52
+ }
53
+
50
54
  body {
51
55
  margin: 0;
52
56
  background: var(--bg);
@@ -454,6 +458,34 @@ progress {
454
458
  border-bottom: 1px solid var(--border);
455
459
  }
456
460
 
461
+ .plain-list .history-entry {
462
+ align-items: flex-start;
463
+ }
464
+
465
+ .history-type {
466
+ flex: 0 0 9rem;
467
+ color: var(--muted);
468
+ }
469
+
470
+ .history-message {
471
+ min-width: 0;
472
+ overflow-wrap: anywhere;
473
+ }
474
+
475
+ @media (max-width: 560px) {
476
+ .plain-list .history-entry {
477
+ flex-wrap: wrap;
478
+ }
479
+
480
+ .history-type {
481
+ flex-basis: auto;
482
+ }
483
+
484
+ .history-message {
485
+ flex-basis: 100%;
486
+ }
487
+ }
488
+
457
489
  .plain-list li:last-child {
458
490
  border-bottom: none;
459
491
  }
@@ -904,6 +936,12 @@ progress {
904
936
  font: inherit;
905
937
  cursor: pointer;
906
938
  display: grid;
939
+ /* O grid externo estica os cards de uma linha para a mesma altura. Sem isto,
940
+ `align-content: stretch` distribui a sobra entre as faixas — inclusive a do
941
+ resumo — e a faixa fica mais alta que as 3 linhas do clamp, deixando vazar
942
+ uma 4ª linha cortada por baixo do reticente. Empacotar no topo manda a
943
+ sobra para o fim do card, que é onde ela não engana ninguém. */
944
+ align-content: start;
907
945
  gap: 10px;
908
946
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
909
947
  }
package/web/public/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Interface do monitoramento web do issue-flow.
2
2
  // JS puro, sem framework e sem recursos externos — funciona offline.
3
- // Consome GET api/sessions, GET api/status (ETag/304) e GET api/health;
3
+ // Consome GET api/sessions, GET api/status (ETag/304), GET api/events e GET api/health;
4
4
  // todo texto dinâmico entra via textContent (nunca innerHTML com dados
5
5
  // do snapshot).
6
6
  (() => {
@@ -79,6 +79,7 @@
79
79
  progressPercent: document.getElementById('progress-percent'),
80
80
  progressCounters: document.getElementById('progress-counters'),
81
81
  now: document.getElementById('now'),
82
+ resilience: document.getElementById('resilience'),
82
83
  phases: document.getElementById('phases'),
83
84
  nextSteps: document.getElementById('next-steps'),
84
85
  stories: document.getElementById('stories'),
@@ -86,6 +87,8 @@
86
87
  pullRequests: document.getElementById('pull-requests'),
87
88
  logFilter: document.getElementById('log-filter'),
88
89
  logs: document.getElementById('logs'),
90
+ historyFilter: document.getElementById('history-filter'),
91
+ history: document.getElementById('history'),
89
92
  sessionMeta: document.getElementById('session-meta'),
90
93
  tabs: Array.prototype.slice.call(document.querySelectorAll('[role="tab"]')),
91
94
  kanban: document.getElementById('kanban'),
@@ -105,6 +108,8 @@
105
108
  polling: false,
106
109
  logFilter: 'all',
107
110
  activeTab: 'tab-execution',
111
+ historyFilter: 'all',
112
+ events: [],
108
113
  // Só o id: o card que abriu o drawer é destruído no próximo render, então
109
114
  // guardar o nó levaria a uma referência morta.
110
115
  selectedStoryId: null,
@@ -116,6 +121,7 @@
116
121
  detailSessionId: null,
117
122
  viewMode: 'detail', // 'detail' | 'dashboard'
118
123
  statusUrl: 'api/status',
124
+ eventsUrl: null,
119
125
  // Se selectSession/clearSessionSelection chega durante um poll, reexecuta.
120
126
  pollAgain: false,
121
127
  };
@@ -372,9 +378,6 @@
372
378
  if (!stillThere) {
373
379
  state.selectedSessionId = null;
374
380
  state.etag = null;
375
- } else if (sessions.length === 1) {
376
- // Seleção explícita deixa de fazer sentido com uma única sessão.
377
- state.selectedSessionId = null;
378
381
  }
379
382
  }
380
383
 
@@ -387,10 +390,6 @@
387
390
  return { mode: 'detail', session: null };
388
391
  }
389
392
 
390
- if (sessions.length === 1) {
391
- return { mode: 'detail', session: sessions[0] };
392
- }
393
-
394
393
  return { mode: 'dashboard', session: null };
395
394
  }
396
395
 
@@ -398,9 +397,9 @@
398
397
  state.viewMode = mode;
399
398
  els.viewDashboard.hidden = mode !== 'dashboard';
400
399
  els.viewDetail.hidden = mode !== 'detail';
401
- // Back quando o usuário escolheu um card e ainda dashboard para voltar.
400
+ // Back quando o usuário escolheu um card e existe uma lista para voltar.
402
401
  const showBack =
403
- mode === 'detail' && state.selectedSessionId !== null && state.sessions.length >= 2;
402
+ mode === 'detail' && state.selectedSessionId !== null && state.sessions.length >= 1;
404
403
  els.backToDashboard.hidden = !showBack;
405
404
  if (mode === 'dashboard' && state.selectedStoryId !== null) {
406
405
  closeDrawer();
@@ -414,10 +413,17 @@
414
413
  return session.statusUrl.replace(/^\//, '');
415
414
  }
416
415
 
416
+ function eventsUrlFor(session) {
417
+ if (!session || !session.eventsUrl) return null;
418
+ return session.eventsUrl.replace(/^\//, '');
419
+ }
420
+
417
421
  function clearDetailState() {
418
422
  state.etag = null;
419
423
  state.snapshot = null;
420
424
  state.detailSessionId = null;
425
+ state.events = [];
426
+ state.eventsUrl = null;
421
427
  }
422
428
 
423
429
  function requestPoll() {
@@ -491,6 +497,7 @@
491
497
  state.statusUrl = nextUrl;
492
498
  state.etag = null;
493
499
  }
500
+ state.eventsUrl = eventsUrlFor(resolved.session);
494
501
 
495
502
  // Evita flash da sessão anterior enquanto o status novo carrega.
496
503
  if (sessionChanged || !state.snapshot) {
@@ -516,6 +523,13 @@
516
523
  state.snapshot = await res.json();
517
524
  render();
518
525
  }
526
+ if (state.eventsUrl) {
527
+ const eventsRes = await fetch(state.eventsUrl, { cache: 'no-store' });
528
+ if (!eventsRes.ok) throw new Error('HTTP ' + eventsRes.status);
529
+ const entries = await eventsRes.json();
530
+ state.events = Array.isArray(entries) ? entries : [];
531
+ renderHistory();
532
+ }
519
533
  setViewMode('detail');
520
534
  state.failures = 0;
521
535
  els.banner.hidden = true;
@@ -614,6 +628,24 @@
614
628
  ? formatAgo(session.startedAt)
615
629
  : '—';
616
630
  meta.appendChild(el('span', null, elapsed));
631
+ // Resilience: how hard this run has had to work, and when it last moved.
632
+ // A card that only shows a percentage cannot tell a run that is
633
+ // progressing from one that has been retrying for twenty minutes.
634
+ if (typeof session.retries === 'number' && session.retries > 0) {
635
+ meta.appendChild(el('span', null, session.retries + ' retry(s)'));
636
+ }
637
+ if (typeof session.correctionCycle === 'number' && session.correctionCycle > 0) {
638
+ meta.appendChild(el('span', null, 'correção ' + session.correctionCycle));
639
+ }
640
+ if (session.updatedAt) {
641
+ meta.appendChild(
642
+ el('span', null, 'atividade ' + formatAgo(session.lastActivityAt || session.updatedAt)),
643
+ );
644
+ }
645
+ if (session.provider) meta.appendChild(el('span', null, 'provider ' + session.provider));
646
+ if (typeof session.attempt === 'number' && session.attempt > 0) {
647
+ meta.appendChild(el('span', null, 'tentativa ' + session.attempt));
648
+ }
617
649
  card.appendChild(meta);
618
650
 
619
651
  const progress = el('span', 'dashboard-progress');
@@ -656,6 +688,7 @@
656
688
  renderRepository(snapshot);
657
689
  renderProgress(snapshot);
658
690
  renderNow(snapshot);
691
+ renderResilience(snapshot);
659
692
  renderPhases(snapshot);
660
693
  renderNextSteps(snapshot);
661
694
  renderStories(snapshot);
@@ -664,6 +697,7 @@
664
697
  renderDrawer(snapshot);
665
698
  renderGit(snapshot);
666
699
  renderLogs(snapshot);
700
+ renderHistory();
667
701
  renderMeta(snapshot);
668
702
  renderTimers();
669
703
  renderTitle(snapshot);
@@ -845,6 +879,26 @@
845
879
  els.now.appendChild(grid);
846
880
  }
847
881
 
882
+ function renderResilience(snapshot) {
883
+ clear(els.resilience);
884
+ const resilience = snapshot.resilience || {};
885
+ const grid = el('dl', 'now-grid');
886
+ nowRow(grid, 'Tentativa', resilience.attempt > 0 ? String(resilience.attempt) : '—');
887
+ nowRow(grid, 'Provider', resilience.provider || '—');
888
+ nowRow(grid, 'Modelo', resilience.model || '—');
889
+ nowRow(grid, 'Última falha', resilience.lastFailureKind || '—');
890
+ nowRow(
891
+ grid,
892
+ 'Cooldown',
893
+ resilience.cooldownUntil ? formatClock(resilience.cooldownUntil) : '—',
894
+ );
895
+ const activity = resilience.lastActivityAt
896
+ ? formatClock(resilience.lastActivityAt) + ' (' + formatAgo(resilience.lastActivityAt) + ')'
897
+ : '—';
898
+ nowRow(grid, 'Última atividade', activity);
899
+ els.resilience.appendChild(grid);
900
+ }
901
+
848
902
  function renderPhases(snapshot) {
849
903
  clear(els.phases);
850
904
  if (snapshot.phases.length === 0) {
@@ -1135,6 +1189,70 @@
1135
1189
  }
1136
1190
  }
1137
1191
 
1192
+ const RESILIENCE_EVENTS = new Set([
1193
+ 'retry',
1194
+ 'agent:attempt',
1195
+ 'agent:activity',
1196
+ 'agent:result',
1197
+ 'failover',
1198
+ ]);
1199
+
1200
+ function historyMessage(event) {
1201
+ switch (event.type) {
1202
+ case 'session:start':
1203
+ return 'Sessão iniciada';
1204
+ case 'session:end':
1205
+ return 'Sessão encerrada: ' + event.status;
1206
+ case 'phase:start':
1207
+ return 'Fase iniciada: ' + event.phase;
1208
+ case 'phase:end':
1209
+ return 'Fase encerrada: ' + event.phase + (event.success ? ' (ok)' : ' (falhou)');
1210
+ case 'iteration:start':
1211
+ return 'Iteração ' + event.iteration + ' iniciada';
1212
+ case 'iteration:end':
1213
+ return 'Iteração ' + event.iteration + ' encerrada';
1214
+ case 'retry':
1215
+ return 'Retry ' + event.attempt + (event.kind ? ': ' + event.kind : '');
1216
+ case 'agent:attempt':
1217
+ return 'Tentativa ' + event.attempt + ' com ' + event.provider;
1218
+ case 'agent:activity':
1219
+ return 'Atividade recebida de ' + event.provider;
1220
+ case 'agent:result':
1221
+ return event.provider + (event.success ? ' concluiu a tentativa' : ' falhou: ' + (event.failureKind || 'desconhecida'));
1222
+ case 'failover':
1223
+ return 'Failover de ' + event.from + ' para ' + event.to + (event.reason ? ': ' + event.reason : '');
1224
+ case 'correction:cycle':
1225
+ return 'Ciclo de correção ' + event.cycle + '/' + event.maxCycles;
1226
+ case 'log':
1227
+ return event.message;
1228
+ default:
1229
+ return event.type;
1230
+ }
1231
+ }
1232
+
1233
+ function renderHistory() {
1234
+ clear(els.history);
1235
+ const entries = state.events.filter((entry) => {
1236
+ const type = entry && entry.event && entry.event.type;
1237
+ if (!type) return false;
1238
+ if (state.historyFilter === 'resilience') return RESILIENCE_EVENTS.has(type);
1239
+ if (state.historyFilter === 'pipeline') return !RESILIENCE_EVENTS.has(type);
1240
+ return true;
1241
+ });
1242
+ if (entries.length === 0) {
1243
+ els.history.appendChild(el('p', 'empty', 'Nenhum evento para exibir.'));
1244
+ return;
1245
+ }
1246
+ for (const entry of entries.slice().reverse()) {
1247
+ const event = entry.event;
1248
+ const item = el('li', 'history-entry');
1249
+ item.appendChild(el('span', 'log-time', formatClock(event.at)));
1250
+ item.appendChild(el('span', 'history-type mono', event.type));
1251
+ item.appendChild(el('span', 'history-message', historyMessage(event)));
1252
+ els.history.appendChild(item);
1253
+ }
1254
+ }
1255
+
1138
1256
  function renderMeta(snapshot) {
1139
1257
  const parts = [];
1140
1258
  if (snapshot.sessionId) parts.push('sessão ' + snapshot.sessionId);
@@ -1201,6 +1319,10 @@
1201
1319
  state.logFilter = els.logFilter.value;
1202
1320
  if (state.snapshot) renderLogs(state.snapshot);
1203
1321
  });
1322
+ els.historyFilter.addEventListener('change', () => {
1323
+ state.historyFilter = els.historyFilter.value;
1324
+ renderHistory();
1325
+ });
1204
1326
 
1205
1327
  document.addEventListener('visibilitychange', () => {
1206
1328
  if (document.hidden) clearTimer();
@@ -69,6 +69,8 @@
69
69
  aria-controls="panel-execution" aria-selected="true">Execução</button>
70
70
  <button type="button" class="tab" role="tab" id="tab-kanban"
71
71
  aria-controls="panel-kanban" aria-selected="false">Kanban</button>
72
+ <button type="button" class="tab" role="tab" id="tab-history"
73
+ aria-controls="panel-history" aria-selected="false">Histórico</button>
72
74
  </div>
73
75
 
74
76
  <div class="tab-panel" id="panel-execution" role="tabpanel" aria-labelledby="tab-execution" tabindex="0">
@@ -97,6 +99,11 @@
97
99
  <div id="now"></div>
98
100
  </section>
99
101
 
102
+ <section class="card" aria-live="polite">
103
+ <h2>Resiliência</h2>
104
+ <div id="resilience"></div>
105
+ </section>
106
+
100
107
  <div class="columns">
101
108
  <section class="card">
102
109
  <h2>Fases</h2>
@@ -148,6 +155,23 @@
148
155
  </section>
149
156
  </div>
150
157
 
158
+ <div class="tab-panel" id="panel-history" role="tabpanel" aria-labelledby="tab-history" tabindex="0" hidden>
159
+ <section class="card">
160
+ <div class="section-head">
161
+ <h2>Histórico da execução</h2>
162
+ <label class="log-filter-control">
163
+ Evento
164
+ <select id="history-filter" aria-label="Filtro do histórico">
165
+ <option value="all">todos</option>
166
+ <option value="resilience">resiliência</option>
167
+ <option value="pipeline">pipeline</option>
168
+ </select>
169
+ </label>
170
+ </div>
171
+ <ol id="history" class="plain-list history"></ol>
172
+ </section>
173
+ </div>
174
+
151
175
  <footer class="muted footer">
152
176
  <span id="session-meta"></span>
153
177
  </footer>
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/commands/analyze.ts"],"sourcesContent":["import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { DEFAULT_HEADLESS_TIMEOUT_MS, runHeadless } from '../core/headless.js';\nimport { applyPlaceholders, loadPrompt } from '../core/prompt-resolver.js';\nimport { publishPhaseMetrics } from '../core/session-metrics.js';\nimport { loadTaskPlan, saveTaskPlan } from '../core/state-manager.js';\nimport { getGlobalTimeout } from '../core/verbose.js';\nimport { issuePlaceholders, resolveCommandIssue } from '../issues/context.js';\nimport type { ResolvedIssue } from '../issues/types.js';\nimport { resolvePolicyPlaceholders } from '../policy/placeholders.js';\nimport { resolveIssuePaths } from '../storage/resolve.js';\nimport { printError, printInfo, printSuccess } from '../ui/logger.js';\n\nexport async function runAnalyze(issue: string, resolvedIssue?: ResolvedIssue): Promise<number> {\n const issueNumber = issue.replace(/^#/, '');\n const paths = await resolveIssuePaths(issueNumber);\n const analysisPath = paths.analysisFile;\n\n const resolution = await resolveCommandIssue(issueNumber, resolvedIssue);\n if (!resolution.ok) {\n return resolution.code;\n }\n\n await mkdir(paths.issueDir, { recursive: true });\n\n const template = await loadPrompt('analyze');\n const prompt = applyPlaceholders(template, {\n // The repository's own conventions. Empty when it declares none, which is\n // what keeps the rendered prompt identical to the pre-policy one.\n ...(await resolvePolicyPlaceholders()),\n __ISSUE_NUMBER__: issueNumber,\n __ANALYSIS_PATH__: analysisPath,\n // Last: the Issue content is substituted in but never scanned again, so a\n // body that happens to contain a placeholder is left untouched.\n ...issuePlaceholders(resolution.resolved),\n });\n\n const startedAtMs = Date.now();\n const result = await runHeadless({\n prompt,\n maxTurns: 30,\n timeout: getGlobalTimeout() ?? DEFAULT_HEADLESS_TIMEOUT_MS,\n // json (not text) so the CLI reports usage: the envelope's `result` field\n // carries the same assistant text this phase already consumed.\n outputFormat: 'json',\n allowedTools: ['Bash', 'Read', 'Glob', 'Grep', 'Write'],\n addDirs: [paths.issueDir],\n statusMessage: `Analyzing issue #${issueNumber}...`,\n });\n // Before the success check: the tokens were spent either way.\n publishPhaseMetrics('analyze', result.cost, startedAtMs);\n\n if (!result.success) {\n printError(`Analysis failed: ${result.error}`);\n return 1;\n }\n\n // Verify the file was created\n try {\n const content = await readFile(analysisPath, 'utf-8');\n if (content.length < 10) {\n printError('Analysis file was created but appears empty');\n return 1;\n }\n } catch {\n // File wasn't created by headless — save the result as analysis\n printInfo('Headless did not create analysis file; saving output directly');\n await writeFile(analysisPath, result.result, 'utf-8');\n }\n\n // Update pipeline state\n const tasksPath = paths.tasksFile;\n try {\n const plan = await loadTaskPlan(tasksPath);\n plan.pipeline.analyzeCompleted = true;\n await saveTaskPlan(tasksPath, plan);\n } catch {\n // tasks.json may not exist yet — that's OK for standalone analyze\n }\n\n printSuccess(`Analysis saved to ${analysisPath}`);\n return 0;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,OAAO,UAAU,iBAAiB;AAY3C,eAAsB,WAAW,OAAe,eAAgD;AAC9F,QAAM,cAAc,MAAM,QAAQ,MAAM,EAAE;AAC1C,QAAM,QAAQ,MAAM,kBAAkB,WAAW;AACjD,QAAM,eAAe,MAAM;AAE3B,QAAM,aAAa,MAAM,oBAAoB,aAAa,aAAa;AACvE,MAAI,CAAC,WAAW,IAAI;AAClB,WAAO,WAAW;AAAA,EACpB;AAEA,QAAM,MAAM,MAAM,UAAU,EAAE,WAAW,KAAK,CAAC;AAE/C,QAAM,WAAW,MAAM,WAAW,SAAS;AAC3C,QAAM,SAAS,kBAAkB,UAAU;AAAA;AAAA;AAAA,IAGzC,GAAI,MAAM,0BAA0B;AAAA,IACpC,kBAAkB;AAAA,IAClB,mBAAmB;AAAA;AAAA;AAAA,IAGnB,GAAG,kBAAkB,WAAW,QAAQ;AAAA,EAC1C,CAAC;AAED,QAAM,cAAc,KAAK,IAAI;AAC7B,QAAM,SAAS,MAAM,YAAY;AAAA,IAC/B;AAAA,IACA,UAAU;AAAA,IACV,SAAS,iBAAiB,KAAK;AAAA;AAAA;AAAA,IAG/B,cAAc;AAAA,IACd,cAAc,CAAC,QAAQ,QAAQ,QAAQ,QAAQ,OAAO;AAAA,IACtD,SAAS,CAAC,MAAM,QAAQ;AAAA,IACxB,eAAe,oBAAoB,WAAW;AAAA,EAChD,CAAC;AAED,sBAAoB,WAAW,OAAO,MAAM,WAAW;AAEvD,MAAI,CAAC,OAAO,SAAS;AACnB,eAAW,oBAAoB,OAAO,KAAK,EAAE;AAC7C,WAAO;AAAA,EACT;AAGA,MAAI;AACF,UAAM,UAAU,MAAM,SAAS,cAAc,OAAO;AACpD,QAAI,QAAQ,SAAS,IAAI;AACvB,iBAAW,6CAA6C;AACxD,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AAEN,cAAU,+DAA+D;AACzE,UAAM,UAAU,cAAc,OAAO,QAAQ,OAAO;AAAA,EACtD;AAGA,QAAM,YAAY,MAAM;AACxB,MAAI;AACF,UAAM,OAAO,MAAM,aAAa,SAAS;AACzC,SAAK,SAAS,mBAAmB;AACjC,UAAM,aAAa,WAAW,IAAI;AAAA,EACpC,QAAQ;AAAA,EAER;AAEA,eAAa,qBAAqB,YAAY,EAAE;AAChD,SAAO;AACT;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/commands/review.ts"],"sourcesContent":["import { DEFAULT_HEADLESS_TIMEOUT_MS, runHeadless } from '../core/headless.js';\nimport { applyPlaceholders, loadPrompt } from '../core/prompt-resolver.js';\nimport { publishPhaseMetrics } from '../core/session-metrics.js';\nimport { loadTaskPlan, saveTaskPlan } from '../core/state-manager.js';\nimport { getGlobalTimeout } from '../core/verbose.js';\nimport { issuePlaceholders, resolveCommandIssue } from '../issues/context.js';\nimport type { ResolvedIssue } from '../issues/types.js';\nimport { resolvePolicyPlaceholders } from '../policy/placeholders.js';\nimport { resolveIssuePaths } from '../storage/resolve.js';\nimport { printError, printSuccess } from '../ui/logger.js';\n\nexport interface ReviewResult {\n status: 'PASS' | 'FAIL';\n findings?: string;\n}\n\n/**\n * Parse the <review-result> block from headless output.\n */\nfunction parseReviewResult(output: string): ReviewResult {\n const match = output.match(/<review-result>([\\s\\S]*?)<\\/review-result>/);\n if (!match) {\n // Try to detect PASS/FAIL from the raw output\n if (/STATUS:\\s*PASS/i.test(output)) {\n return { status: 'PASS' };\n }\n if (/STATUS:\\s*FAIL/i.test(output)) {\n const findingsMatch = output.match(/FINDINGS:([\\s\\S]*?)(?:$|<\\/)/);\n return { status: 'FAIL', findings: findingsMatch?.[1]?.trim() ?? 'Unknown findings' };\n }\n // Default to PASS if no explicit status found and no errors\n return { status: 'PASS' };\n }\n\n const block = match[1];\n if (/STATUS:\\s*PASS/i.test(block)) {\n return { status: 'PASS' };\n }\n\n const findingsMatch = block.match(/FINDINGS:([\\s\\S]*)/);\n return {\n status: 'FAIL',\n findings: findingsMatch?.[1]?.trim() ?? 'Unknown findings',\n };\n}\n\nexport async function runReview(issue: string, resolvedIssue?: ResolvedIssue): Promise<number> {\n const issueNumber = issue.replace(/^#/, '');\n const paths = await resolveIssuePaths(issueNumber);\n const tasksPath = paths.tasksFile;\n\n const resolution = await resolveCommandIssue(issueNumber, resolvedIssue);\n if (!resolution.ok) {\n return resolution.code;\n }\n\n const template = await loadPrompt('review');\n const prompt = applyPlaceholders(template, {\n // The repository's own conventions. Empty when it declares none, which is\n // what keeps the rendered prompt identical to the pre-policy one.\n ...(await resolvePolicyPlaceholders()),\n __ISSUE_NUMBER__: issueNumber,\n __TASKS_PATH__: tasksPath,\n ...issuePlaceholders(resolution.resolved),\n });\n\n const startedAtMs = Date.now();\n const result = await runHeadless({\n prompt,\n maxTurns: 25,\n timeout: getGlobalTimeout() ?? DEFAULT_HEADLESS_TIMEOUT_MS,\n // json (not text) so the CLI reports usage: the envelope's `result` field\n // carries the same assistant text parseReviewResult() already consumed.\n outputFormat: 'json',\n allowedTools: ['Bash', 'Read', 'Glob', 'Grep'],\n addDirs: [paths.issueDir],\n statusMessage: `Reviewing issue #${issueNumber} resolution...`,\n });\n // Before the success check: the tokens were spent either way. A correction\n // cycle calls runReview() again and the reducer sums both invocations.\n publishPhaseMetrics('review', result.cost, startedAtMs);\n\n if (!result.success) {\n printError(`Review failed: ${result.error}`);\n return 1;\n }\n\n const review = parseReviewResult(result.result);\n\n if (review.status === 'PASS') {\n // Update pipeline state\n try {\n const plan = await loadTaskPlan(tasksPath);\n plan.pipeline.reviewCompleted = true;\n plan.lastReviewFindings = null;\n await saveTaskPlan(tasksPath, plan);\n } catch {\n // tasks.json may not exist\n }\n\n printSuccess('Review PASSED — implementation meets acceptance criteria');\n return 0;\n }\n\n printError('Review FAILED — issues found:');\n if (review.findings) {\n console.log(review.findings);\n }\n\n // Persist the findings so a correction cycle can act on them: without this,\n // a re-run of execute sees every userStories[].passes already true and\n // exits immediately without attempting any fix (see engine.ts's\n // \"already complete\" guards).\n try {\n const plan = await loadTaskPlan(tasksPath);\n plan.pipeline.reviewCompleted = false;\n plan.lastReviewFindings = review.findings ?? 'Unknown findings';\n await saveTaskPlan(tasksPath, plan);\n } catch {\n // tasks.json may not exist\n }\n\n return 1;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmBA,SAAS,kBAAkB,QAA8B;AACvD,QAAM,QAAQ,OAAO,MAAM,4CAA4C;AACvE,MAAI,CAAC,OAAO;AAEV,QAAI,kBAAkB,KAAK,MAAM,GAAG;AAClC,aAAO,EAAE,QAAQ,OAAO;AAAA,IAC1B;AACA,QAAI,kBAAkB,KAAK,MAAM,GAAG;AAClC,YAAMA,iBAAgB,OAAO,MAAM,8BAA8B;AACjE,aAAO,EAAE,QAAQ,QAAQ,UAAUA,iBAAgB,CAAC,GAAG,KAAK,KAAK,mBAAmB;AAAA,IACtF;AAEA,WAAO,EAAE,QAAQ,OAAO;AAAA,EAC1B;AAEA,QAAM,QAAQ,MAAM,CAAC;AACrB,MAAI,kBAAkB,KAAK,KAAK,GAAG;AACjC,WAAO,EAAE,QAAQ,OAAO;AAAA,EAC1B;AAEA,QAAM,gBAAgB,MAAM,MAAM,oBAAoB;AACtD,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,UAAU,gBAAgB,CAAC,GAAG,KAAK,KAAK;AAAA,EAC1C;AACF;AAEA,eAAsB,UAAU,OAAe,eAAgD;AAC7F,QAAM,cAAc,MAAM,QAAQ,MAAM,EAAE;AAC1C,QAAM,QAAQ,MAAM,kBAAkB,WAAW;AACjD,QAAM,YAAY,MAAM;AAExB,QAAM,aAAa,MAAM,oBAAoB,aAAa,aAAa;AACvE,MAAI,CAAC,WAAW,IAAI;AAClB,WAAO,WAAW;AAAA,EACpB;AAEA,QAAM,WAAW,MAAM,WAAW,QAAQ;AAC1C,QAAM,SAAS,kBAAkB,UAAU;AAAA;AAAA;AAAA,IAGzC,GAAI,MAAM,0BAA0B;AAAA,IACpC,kBAAkB;AAAA,IAClB,gBAAgB;AAAA,IAChB,GAAG,kBAAkB,WAAW,QAAQ;AAAA,EAC1C,CAAC;AAED,QAAM,cAAc,KAAK,IAAI;AAC7B,QAAM,SAAS,MAAM,YAAY;AAAA,IAC/B;AAAA,IACA,UAAU;AAAA,IACV,SAAS,iBAAiB,KAAK;AAAA;AAAA;AAAA,IAG/B,cAAc;AAAA,IACd,cAAc,CAAC,QAAQ,QAAQ,QAAQ,MAAM;AAAA,IAC7C,SAAS,CAAC,MAAM,QAAQ;AAAA,IACxB,eAAe,oBAAoB,WAAW;AAAA,EAChD,CAAC;AAGD,sBAAoB,UAAU,OAAO,MAAM,WAAW;AAEtD,MAAI,CAAC,OAAO,SAAS;AACnB,eAAW,kBAAkB,OAAO,KAAK,EAAE;AAC3C,WAAO;AAAA,EACT;AAEA,QAAM,SAAS,kBAAkB,OAAO,MAAM;AAE9C,MAAI,OAAO,WAAW,QAAQ;AAE5B,QAAI;AACF,YAAM,OAAO,MAAM,aAAa,SAAS;AACzC,WAAK,SAAS,kBAAkB;AAChC,WAAK,qBAAqB;AAC1B,YAAM,aAAa,WAAW,IAAI;AAAA,IACpC,QAAQ;AAAA,IAER;AAEA,iBAAa,+DAA0D;AACvE,WAAO;AAAA,EACT;AAEA,aAAW,oCAA+B;AAC1C,MAAI,OAAO,UAAU;AACnB,YAAQ,IAAI,OAAO,QAAQ;AAAA,EAC7B;AAMA,MAAI;AACF,UAAM,OAAO,MAAM,aAAa,SAAS;AACzC,SAAK,SAAS,kBAAkB;AAChC,SAAK,qBAAqB,OAAO,YAAY;AAC7C,UAAM,aAAa,WAAW,IAAI;AAAA,EACpC,QAAQ;AAAA,EAER;AAEA,SAAO;AACT;","names":["findingsMatch"]}
@@ -1,58 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- retryDelaySeconds,
4
- sleep
5
- } from "./chunk-HDV3YDTG.js";
6
- import {
7
- getSessionPublisher,
8
- isoNow,
9
- printRetry
10
- } from "./chunk-HC2MT7BX.js";
11
-
12
- // src/core/phase-runner.ts
13
- import { readFile } from "fs/promises";
14
- var DEFAULT_PHASE_RETRY_LIMIT = 3;
15
- var DEFAULT_BACKOFF_BASE_SECONDS = 15;
16
- var DEFAULT_BACKOFF_MAX_SECONDS = 120;
17
- async function runPhaseWithRetry(options) {
18
- const retryLimit = options.retryLimit ?? DEFAULT_PHASE_RETRY_LIMIT;
19
- const backoffBaseSeconds = options.backoffBaseSeconds ?? DEFAULT_BACKOFF_BASE_SECONDS;
20
- const backoffMaxSeconds = options.backoffMaxSeconds ?? DEFAULT_BACKOFF_MAX_SECONDS;
21
- let lastResult = { ok: false, error: "Phase never attempted" };
22
- let retryCount = 0;
23
- for (let attemptNumber = 1; attemptNumber <= retryLimit; attemptNumber++) {
24
- lastResult = await options.attempt(attemptNumber);
25
- if (lastResult.ok) return lastResult;
26
- if (!lastResult.transient || attemptNumber === retryLimit) return lastResult;
27
- retryCount++;
28
- const delaySeconds = retryDelaySeconds(retryCount, backoffBaseSeconds, backoffMaxSeconds);
29
- getSessionPublisher().publish({
30
- type: "retry",
31
- at: isoNow(),
32
- attempt: retryCount,
33
- delaySeconds,
34
- reason: lastResult.error
35
- });
36
- printRetry(
37
- `Phase ${options.phase} hit a recoverable failure (attempt ${attemptNumber}/${retryLimit}): ${lastResult.error}. Retrying in ${delaySeconds}s.`
38
- );
39
- await sleep(delaySeconds);
40
- }
41
- return lastResult;
42
- }
43
- async function readFileWithGrace(path, delaysMs = [300, 800]) {
44
- for (let i = 0; ; i++) {
45
- try {
46
- return await readFile(path, "utf-8");
47
- } catch (err) {
48
- if (i >= delaysMs.length) throw err;
49
- await new Promise((resolve) => setTimeout(resolve, delaysMs[i]));
50
- }
51
- }
52
- }
53
-
54
- export {
55
- runPhaseWithRetry,
56
- readFileWithGrace
57
- };
58
- //# sourceMappingURL=chunk-3BOHTH4V.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/core/phase-runner.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { printRetry } from '../ui/logger.js';\nimport { retryDelaySeconds, sleep } from '../utils/retry.js';\nimport { getSessionPublisher } from './session-publisher.js';\nimport { isoNow } from './state-manager.js';\n\nexport interface PhaseAttemptResult {\n ok: boolean;\n /** Whether a failed attempt is worth retrying. Ignored when ok is true. */\n transient?: boolean;\n error?: string;\n}\n\nexport interface PhaseRetryOptions {\n phase: string;\n attempt: (attemptNumber: number) => Promise<PhaseAttemptResult>;\n retryLimit?: number;\n backoffBaseSeconds?: number;\n backoffMaxSeconds?: number;\n}\n\nconst DEFAULT_PHASE_RETRY_LIMIT = 3;\nconst DEFAULT_BACKOFF_BASE_SECONDS = 15;\nconst DEFAULT_BACKOFF_MAX_SECONDS = 120;\n\n/**\n * Bounded retry wrapper for single-shot phases (prd, plan, pr) that invoke\n * the Claude CLI once and validate an artifact. Mirrors the transient-retry\n * pattern already used by the execute phase's iteration loop (core/engine.ts:\n * isTransientFailure + retryDelaySeconds), applied here to phases that\n * previously aborted the whole pipeline on the first failure with no chance\n * to recover. Non-transient failures (or exhausting the retry budget) still\n * end the phase — this bounds recovery attempts, it never retries forever.\n */\nexport async function runPhaseWithRetry(options: PhaseRetryOptions): Promise<PhaseAttemptResult> {\n const retryLimit = options.retryLimit ?? DEFAULT_PHASE_RETRY_LIMIT;\n const backoffBaseSeconds = options.backoffBaseSeconds ?? DEFAULT_BACKOFF_BASE_SECONDS;\n const backoffMaxSeconds = options.backoffMaxSeconds ?? DEFAULT_BACKOFF_MAX_SECONDS;\n\n let lastResult: PhaseAttemptResult = { ok: false, error: 'Phase never attempted' };\n let retryCount = 0;\n\n for (let attemptNumber = 1; attemptNumber <= retryLimit; attemptNumber++) {\n lastResult = await options.attempt(attemptNumber);\n if (lastResult.ok) return lastResult;\n if (!lastResult.transient || attemptNumber === retryLimit) return lastResult;\n\n retryCount++;\n const delaySeconds = retryDelaySeconds(retryCount, backoffBaseSeconds, backoffMaxSeconds);\n getSessionPublisher().publish({\n type: 'retry',\n at: isoNow(),\n attempt: retryCount,\n delaySeconds,\n reason: lastResult.error,\n });\n printRetry(\n `Phase ${options.phase} hit a recoverable failure (attempt ${attemptNumber}/${retryLimit}): ${lastResult.error}. Retrying in ${delaySeconds}s.`,\n );\n await sleep(delaySeconds);\n }\n\n return lastResult;\n}\n\n/**\n * Re-read a file a few times with short delays before giving up. Absorbs a\n * brief filesystem-visibility lag between the Claude CLI's Write tool\n * completing and this process observing the result, without spending a full\n * phase retry (and a new Claude invocation) on what may just be a timing gap.\n */\nexport async function readFileWithGrace(\n path: string,\n delaysMs: number[] = [300, 800],\n): Promise<string> {\n for (let i = 0; ; i++) {\n try {\n return await readFile(path, 'utf-8');\n } catch (err) {\n if (i >= delaysMs.length) throw err;\n await new Promise((resolve) => setTimeout(resolve, delaysMs[i]));\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;AAAA,SAAS,gBAAgB;AAqBzB,IAAM,4BAA4B;AAClC,IAAM,+BAA+B;AACrC,IAAM,8BAA8B;AAWpC,eAAsB,kBAAkB,SAAyD;AAC/F,QAAM,aAAa,QAAQ,cAAc;AACzC,QAAM,qBAAqB,QAAQ,sBAAsB;AACzD,QAAM,oBAAoB,QAAQ,qBAAqB;AAEvD,MAAI,aAAiC,EAAE,IAAI,OAAO,OAAO,wBAAwB;AACjF,MAAI,aAAa;AAEjB,WAAS,gBAAgB,GAAG,iBAAiB,YAAY,iBAAiB;AACxE,iBAAa,MAAM,QAAQ,QAAQ,aAAa;AAChD,QAAI,WAAW,GAAI,QAAO;AAC1B,QAAI,CAAC,WAAW,aAAa,kBAAkB,WAAY,QAAO;AAElE;AACA,UAAM,eAAe,kBAAkB,YAAY,oBAAoB,iBAAiB;AACxF,wBAAoB,EAAE,QAAQ;AAAA,MAC5B,MAAM;AAAA,MACN,IAAI,OAAO;AAAA,MACX,SAAS;AAAA,MACT;AAAA,MACA,QAAQ,WAAW;AAAA,IACrB,CAAC;AACD;AAAA,MACE,SAAS,QAAQ,KAAK,uCAAuC,aAAa,IAAI,UAAU,MAAM,WAAW,KAAK,iBAAiB,YAAY;AAAA,IAC7I;AACA,UAAM,MAAM,YAAY;AAAA,EAC1B;AAEA,SAAO;AACT;AAQA,eAAsB,kBACpB,MACA,WAAqB,CAAC,KAAK,GAAG,GACb;AACjB,WAAS,IAAI,KAAK,KAAK;AACrB,QAAI;AACF,aAAO,MAAM,SAAS,MAAM,OAAO;AAAA,IACrC,SAAS,KAAK;AACZ,UAAI,KAAK,SAAS,OAAQ,OAAM;AAChC,YAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,SAAS,CAAC,CAAC,CAAC;AAAA,IACjE;AAAA,EACF;AACF;","names":[]}