@shepai/cli 1.52.2 → 1.53.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 (179) hide show
  1. package/apis/json-schema/CiFixRecord.yaml +24 -0
  2. package/apis/json-schema/Feature.yaml +3 -0
  3. package/apis/json-schema/PullRequest.yaml +10 -0
  4. package/apis/json-schema/SdlcLifecycle.yaml +1 -0
  5. package/apis/json-schema/WorkflowConfig.yaml +15 -0
  6. package/dist/packages/core/src/application/ports/output/repositories/feature-repository.interface.d.ts +8 -0
  7. package/dist/packages/core/src/application/ports/output/repositories/feature-repository.interface.d.ts.map +1 -1
  8. package/dist/packages/core/src/application/ports/output/services/git-pr-service.interface.d.ts +12 -0
  9. package/dist/packages/core/src/application/ports/output/services/git-pr-service.interface.d.ts.map +1 -1
  10. package/dist/packages/core/src/application/use-cases/features/check-and-unblock-features.use-case.d.ts +30 -0
  11. package/dist/packages/core/src/application/use-cases/features/check-and-unblock-features.use-case.d.ts.map +1 -0
  12. package/dist/packages/core/src/application/use-cases/features/check-and-unblock-features.use-case.js +74 -0
  13. package/dist/packages/core/src/application/use-cases/features/create/create-feature.use-case.d.ts.map +1 -1
  14. package/dist/packages/core/src/application/use-cases/features/create/create-feature.use-case.js +60 -16
  15. package/dist/packages/core/src/application/use-cases/features/create/types.d.ts +2 -0
  16. package/dist/packages/core/src/application/use-cases/features/create/types.d.ts.map +1 -1
  17. package/dist/packages/core/src/application/use-cases/features/delete-feature.use-case.d.ts.map +1 -1
  18. package/dist/packages/core/src/application/use-cases/features/delete-feature.use-case.js +11 -4
  19. package/dist/packages/core/src/application/use-cases/features/update/update-feature-lifecycle.use-case.d.ts +28 -0
  20. package/dist/packages/core/src/application/use-cases/features/update/update-feature-lifecycle.use-case.d.ts.map +1 -0
  21. package/dist/packages/core/src/application/use-cases/features/update/update-feature-lifecycle.use-case.js +53 -0
  22. package/dist/packages/core/src/domain/generated/output.d.ts +47 -1
  23. package/dist/packages/core/src/domain/generated/output.d.ts.map +1 -1
  24. package/dist/packages/core/src/domain/generated/output.js +1 -0
  25. package/dist/packages/core/src/domain/lifecycle-gates.d.ts +16 -0
  26. package/dist/packages/core/src/domain/lifecycle-gates.d.ts.map +1 -0
  27. package/dist/packages/core/src/domain/lifecycle-gates.js +19 -0
  28. package/dist/packages/core/src/infrastructure/di/container.d.ts.map +1 -1
  29. package/dist/packages/core/src/infrastructure/di/container.js +12 -0
  30. package/dist/packages/core/src/infrastructure/persistence/sqlite/mappers/feature.mapper.d.ts +3 -0
  31. package/dist/packages/core/src/infrastructure/persistence/sqlite/mappers/feature.mapper.d.ts.map +1 -1
  32. package/dist/packages/core/src/infrastructure/persistence/sqlite/mappers/feature.mapper.js +8 -0
  33. package/dist/packages/core/src/infrastructure/persistence/sqlite/migrations.d.ts.map +1 -1
  34. package/dist/packages/core/src/infrastructure/persistence/sqlite/migrations.js +28 -1
  35. package/dist/packages/core/src/infrastructure/repositories/sqlite-feature.repository.d.ts +1 -0
  36. package/dist/packages/core/src/infrastructure/repositories/sqlite-feature.repository.d.ts.map +1 -1
  37. package/dist/packages/core/src/infrastructure/repositories/sqlite-feature.repository.js +12 -0
  38. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-graph.d.ts +52 -1
  39. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-graph.d.ts.map +1 -1
  40. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-graph.js +1 -1
  41. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-worker.d.ts.map +1 -1
  42. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-worker.js +7 -2
  43. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/lifecycle-context.d.ts +27 -5
  44. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/lifecycle-context.d.ts.map +1 -1
  45. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/lifecycle-context.js +19 -16
  46. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/ci-helpers.d.ts +24 -0
  47. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/ci-helpers.d.ts.map +1 -0
  48. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/ci-helpers.js +47 -0
  49. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/ci-watch-fix-loop.d.ts +43 -0
  50. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/ci-watch-fix-loop.d.ts.map +1 -0
  51. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/ci-watch-fix-loop.js +118 -0
  52. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/merge-output-parser.d.ts.map +1 -0
  53. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/{merge.node.d.ts → merge/merge.node.d.ts} +10 -4
  54. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/merge.node.d.ts.map +1 -0
  55. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/{merge.node.js → merge/merge.node.js} +44 -17
  56. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/prompts/merge-prompts.d.ts +12 -0
  57. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/prompts/merge-prompts.d.ts.map +1 -1
  58. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/prompts/merge-prompts.js +38 -0
  59. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/state.d.ts +4 -1
  60. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/state.d.ts.map +1 -1
  61. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/state.js +13 -0
  62. package/dist/packages/core/src/infrastructure/services/git/git-pr.service.d.ts +2 -0
  63. package/dist/packages/core/src/infrastructure/services/git/git-pr.service.d.ts.map +1 -1
  64. package/dist/packages/core/src/infrastructure/services/git/git-pr.service.js +29 -1
  65. package/dist/src/presentation/cli/commands/feat/ls.command.d.ts.map +1 -1
  66. package/dist/src/presentation/cli/commands/feat/ls.command.js +39 -3
  67. package/dist/src/presentation/cli/commands/feat/new.command.d.ts.map +1 -1
  68. package/dist/src/presentation/cli/commands/feat/new.command.js +18 -0
  69. package/dist/src/presentation/cli/commands/feat/show.command.d.ts.map +1 -1
  70. package/dist/src/presentation/cli/commands/feat/show.command.js +20 -0
  71. package/dist/src/presentation/web/app/actions/create-feature.d.ts +1 -0
  72. package/dist/src/presentation/web/app/actions/create-feature.d.ts.map +1 -1
  73. package/dist/src/presentation/web/app/actions/create-feature.js +2 -1
  74. package/dist/src/presentation/web/app/page.d.ts.map +1 -1
  75. package/dist/src/presentation/web/app/page.js +34 -6
  76. package/dist/src/presentation/web/components/common/feature-create-drawer/feature-create-drawer.d.ts +11 -1
  77. package/dist/src/presentation/web/components/common/feature-create-drawer/feature-create-drawer.d.ts.map +1 -1
  78. package/dist/src/presentation/web/components/common/feature-create-drawer/feature-create-drawer.js +52 -5
  79. package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.d.ts.map +1 -1
  80. package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.js +4 -0
  81. package/dist/src/presentation/web/components/common/feature-node/feature-node.js +1 -1
  82. package/dist/src/presentation/web/components/features/control-center/control-center-inner.d.ts.map +1 -1
  83. package/dist/src/presentation/web/components/features/control-center/control-center-inner.js +13 -4
  84. package/dist/src/presentation/web/components/features/control-center/use-control-center-state.d.ts +2 -0
  85. package/dist/src/presentation/web/components/features/control-center/use-control-center-state.d.ts.map +1 -1
  86. package/dist/src/presentation/web/components/features/control-center/use-control-center-state.js +26 -8
  87. package/dist/src/presentation/web/components/features/features-canvas/dependency-edge.d.ts +7 -0
  88. package/dist/src/presentation/web/components/features/features-canvas/dependency-edge.d.ts.map +1 -0
  89. package/dist/src/presentation/web/components/features/features-canvas/dependency-edge.js +20 -0
  90. package/dist/src/presentation/web/components/features/features-canvas/dependency-edge.stories.d.ts +6 -0
  91. package/dist/src/presentation/web/components/features/features-canvas/dependency-edge.stories.d.ts.map +1 -0
  92. package/dist/src/presentation/web/components/features/features-canvas/dependency-edge.stories.js +58 -0
  93. package/dist/src/presentation/web/components/features/features-canvas/features-canvas.d.ts.map +1 -1
  94. package/dist/src/presentation/web/components/features/features-canvas/features-canvas.js +5 -1
  95. package/dist/tsconfig.build.tsbuildinfo +1 -1
  96. package/package.json +1 -1
  97. package/web/.next/BUILD_ID +1 -1
  98. package/web/.next/build-manifest.json +2 -2
  99. package/web/.next/cache/.previewinfo +1 -1
  100. package/web/.next/cache/.rscinfo +1 -1
  101. package/web/.next/cache/.tsbuildinfo +1 -1
  102. package/web/.next/cache/config.json +3 -3
  103. package/web/.next/fallback-build-manifest.json +2 -2
  104. package/web/.next/prerender-manifest.json +3 -3
  105. package/web/.next/required-server-files.js +1 -1
  106. package/web/.next/required-server-files.json +1 -1
  107. package/web/.next/server/app/_global-error.html +2 -2
  108. package/web/.next/server/app/_global-error.rsc +1 -1
  109. package/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  110. package/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  111. package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  112. package/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  113. package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  114. package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  115. package/web/.next/server/app/page/server-reference-manifest.json +14 -14
  116. package/web/.next/server/app/page.js.nft.json +1 -1
  117. package/web/.next/server/app/page_client-reference-manifest.js +1 -1
  118. package/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
  119. package/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js +2 -2
  120. package/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js.map +1 -1
  121. package/web/.next/server/chunks/ssr/[root-of-the-server]__249c74f6._.js +1 -1
  122. package/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js +2 -2
  123. package/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js.map +1 -1
  124. package/web/.next/server/chunks/ssr/_1fafa9db._.js +1 -1
  125. package/web/.next/server/chunks/ssr/_1fafa9db._.js.map +1 -1
  126. package/web/.next/server/chunks/ssr/src_presentation_web_ed0934e5._.js +1 -1
  127. package/web/.next/server/chunks/ssr/src_presentation_web_ed0934e5._.js.map +1 -1
  128. package/web/.next/server/pages/500.html +2 -2
  129. package/web/.next/server/server-reference-manifest.js +1 -1
  130. package/web/.next/server/server-reference-manifest.json +15 -15
  131. package/web/.next/standalone/src/presentation/web/.next/BUILD_ID +1 -1
  132. package/web/.next/standalone/src/presentation/web/.next/build-manifest.json +2 -2
  133. package/web/.next/standalone/src/presentation/web/.next/prerender-manifest.json +3 -3
  134. package/web/.next/standalone/src/presentation/web/.next/required-server-files.json +1 -1
  135. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.html +2 -2
  136. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.rsc +1 -1
  137. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  138. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  139. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  140. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  141. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  142. package/web/.next/standalone/src/presentation/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  143. package/web/.next/standalone/src/presentation/web/.next/server/app/page/server-reference-manifest.json +14 -14
  144. package/web/.next/standalone/src/presentation/web/.next/server/app/page.js.nft.json +1 -1
  145. package/web/.next/standalone/src/presentation/web/.next/server/app/page_client-reference-manifest.js +1 -1
  146. package/web/.next/standalone/src/presentation/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
  147. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js +2 -2
  148. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__249c74f6._.js +1 -1
  149. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js +2 -2
  150. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/_1fafa9db._.js +1 -1
  151. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web_ed0934e5._.js +1 -1
  152. package/web/.next/standalone/src/presentation/web/.next/server/pages/500.html +2 -2
  153. package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.js +1 -1
  154. package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.json +15 -15
  155. package/web/.next/standalone/src/presentation/web/app/actions/create-feature.ts +4 -1
  156. package/web/.next/standalone/src/presentation/web/app/page.tsx +36 -6
  157. package/web/.next/standalone/src/presentation/web/components/common/feature-create-drawer/feature-create-drawer.tsx +199 -2
  158. package/web/.next/standalone/src/presentation/web/components/common/feature-node/derive-feature-state.ts +5 -0
  159. package/web/.next/standalone/src/presentation/web/components/common/feature-node/feature-node.tsx +1 -1
  160. package/web/.next/standalone/src/presentation/web/components/features/control-center/control-center-inner.tsx +20 -1
  161. package/web/.next/standalone/src/presentation/web/components/features/control-center/use-control-center-state.ts +43 -12
  162. package/web/.next/standalone/src/presentation/web/components/features/features-canvas/dependency-edge.stories.tsx +81 -0
  163. package/web/.next/standalone/src/presentation/web/components/features/features-canvas/dependency-edge.tsx +29 -0
  164. package/web/.next/standalone/src/presentation/web/components/features/features-canvas/features-canvas.tsx +9 -0
  165. package/web/.next/standalone/src/presentation/web/server.js +1 -1
  166. package/web/.next/static/chunks/48309f9ad2a84b15.css +2 -0
  167. package/web/.next/static/chunks/5123ae1490fd2606.js +10 -0
  168. package/web/.next/static/chunks/{31e5030eed01a941.js → 68fb91b6eca14536.js} +1 -1
  169. package/web/.next/trace +1 -1
  170. package/web/.next/trace-build +1 -1
  171. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge-output-parser.d.ts.map +0 -1
  172. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge.node.d.ts.map +0 -1
  173. package/web/.next/static/chunks/23cae32e6f42833d.js +0 -10
  174. package/web/.next/static/chunks/59b8bd47023e0249.css +0 -2
  175. /package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/{merge-output-parser.d.ts → merge/merge-output-parser.d.ts} +0 -0
  176. /package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/{merge-output-parser.js → merge/merge-output-parser.js} +0 -0
  177. /package/web/.next/static/{-c2lwSbfsfZHNnfB5CogT → OXSWdTgy-1XJBN9IJtQPz}/_buildManifest.js +0 -0
  178. /package/web/.next/static/{-c2lwSbfsfZHNnfB5CogT → OXSWdTgy-1XJBN9IJtQPz}/_clientMiddlewareManifest.json +0 -0
  179. /package/web/.next/static/{-c2lwSbfsfZHNnfB5CogT → OXSWdTgy-1XJBN9IJtQPz}/_ssgManifest.js +0 -0
@@ -5,22 +5,44 @@
5
5
  * SDLC lifecycle as they execute, following the same pattern as
6
6
  * phase-timing-context.ts.
7
7
  *
8
- * The worker calls setLifecycleContext() once after DI init.
9
- * Nodes call updateNodeLifecycle() at the start of execution.
10
- * Errors are swallowed so lifecycle updates never block graph execution.
8
+ * The worker calls setLifecycleContext() once after DI init, passing an
9
+ * UpdateFeatureLifecycleUseCase instance. Nodes call updateNodeLifecycle()
10
+ * at the start of execution. Errors are swallowed so lifecycle updates
11
+ * never block graph execution.
12
+ *
13
+ * Routing lifecycle updates through UpdateFeatureLifecycleUseCase ensures
14
+ * that CheckAndUnblockFeaturesUseCase fires on every transition,
15
+ * automatically unblocking blocked children that are now eligible to start.
16
+ */
17
+ import { SdlcLifecycle } from '../../../../domain/generated/output.js';
18
+ /**
19
+ * Minimal interface satisfied by UpdateFeatureLifecycleUseCase.
20
+ * Using a structural interface avoids a concrete import between infrastructure
21
+ * and application layer classes.
11
22
  */
12
- import type { IFeatureRepository } from '../../../../application/ports/output/repositories/feature-repository.interface.js';
23
+ interface LifecycleUpdater {
24
+ execute(input: {
25
+ featureId: string;
26
+ lifecycle: SdlcLifecycle;
27
+ }): Promise<void>;
28
+ }
13
29
  /**
14
30
  * Set the lifecycle context. Called once by the worker after DI init.
31
+ *
32
+ * @param featureId - The feature being processed.
33
+ * @param updater - An UpdateFeatureLifecycleUseCase instance (or compatible updater).
15
34
  */
16
- export declare function setLifecycleContext(featureId: string, repository: IFeatureRepository): void;
35
+ export declare function setLifecycleContext(featureId: string, updater: LifecycleUpdater): void;
17
36
  /**
18
37
  * Clear the lifecycle context. Useful for testing.
19
38
  */
20
39
  export declare function clearLifecycleContext(): void;
21
40
  /**
22
41
  * Update the feature's lifecycle to match the current graph node.
42
+ * Routes through UpdateFeatureLifecycleUseCase so that
43
+ * CheckAndUnblockFeaturesUseCase fires on every transition.
23
44
  * No-op if context is not set or the node name has no lifecycle mapping.
24
45
  */
25
46
  export declare function updateNodeLifecycle(nodeName: string): Promise<void>;
47
+ export {};
26
48
  //# sourceMappingURL=lifecycle-context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"lifecycle-context.d.ts","sourceRoot":"","sources":["../../../../../../../../packages/core/src/infrastructure/services/agents/feature-agent/lifecycle-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yEAAyE,CAAC;AAgBlH;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,kBAAkB,GAAG,IAAI,CAG3F;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBzE"}
1
+ {"version":3,"file":"lifecycle-context.d.ts","sourceRoot":"","sources":["../../../../../../../../packages/core/src/infrastructure/services/agents/feature-agent/lifecycle-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAE7D;;;;GAIG;AACH,UAAU,gBAAgB;IACxB,OAAO,CAAC,KAAK,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,aAAa,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChF;AAeD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAGtF;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWzE"}
@@ -5,13 +5,18 @@
5
5
  * SDLC lifecycle as they execute, following the same pattern as
6
6
  * phase-timing-context.ts.
7
7
  *
8
- * The worker calls setLifecycleContext() once after DI init.
9
- * Nodes call updateNodeLifecycle() at the start of execution.
10
- * Errors are swallowed so lifecycle updates never block graph execution.
8
+ * The worker calls setLifecycleContext() once after DI init, passing an
9
+ * UpdateFeatureLifecycleUseCase instance. Nodes call updateNodeLifecycle()
10
+ * at the start of execution. Errors are swallowed so lifecycle updates
11
+ * never block graph execution.
12
+ *
13
+ * Routing lifecycle updates through UpdateFeatureLifecycleUseCase ensures
14
+ * that CheckAndUnblockFeaturesUseCase fires on every transition,
15
+ * automatically unblocking blocked children that are now eligible to start.
11
16
  */
12
17
  import { SdlcLifecycle } from '../../../../domain/generated/output.js';
13
18
  let contextFeatureId;
14
- let contextRepository;
19
+ let contextUpdater;
15
20
  /** Map graph node names to their corresponding SDLC lifecycle stage. */
16
21
  const NODE_TO_LIFECYCLE = {
17
22
  analyze: SdlcLifecycle.Analyze,
@@ -23,37 +28,35 @@ const NODE_TO_LIFECYCLE = {
23
28
  };
24
29
  /**
25
30
  * Set the lifecycle context. Called once by the worker after DI init.
31
+ *
32
+ * @param featureId - The feature being processed.
33
+ * @param updater - An UpdateFeatureLifecycleUseCase instance (or compatible updater).
26
34
  */
27
- export function setLifecycleContext(featureId, repository) {
35
+ export function setLifecycleContext(featureId, updater) {
28
36
  contextFeatureId = featureId;
29
- contextRepository = repository;
37
+ contextUpdater = updater;
30
38
  }
31
39
  /**
32
40
  * Clear the lifecycle context. Useful for testing.
33
41
  */
34
42
  export function clearLifecycleContext() {
35
43
  contextFeatureId = undefined;
36
- contextRepository = undefined;
44
+ contextUpdater = undefined;
37
45
  }
38
46
  /**
39
47
  * Update the feature's lifecycle to match the current graph node.
48
+ * Routes through UpdateFeatureLifecycleUseCase so that
49
+ * CheckAndUnblockFeaturesUseCase fires on every transition.
40
50
  * No-op if context is not set or the node name has no lifecycle mapping.
41
51
  */
42
52
  export async function updateNodeLifecycle(nodeName) {
43
- if (!contextFeatureId || !contextRepository)
53
+ if (!contextFeatureId || !contextUpdater)
44
54
  return;
45
55
  const lifecycle = NODE_TO_LIFECYCLE[nodeName];
46
56
  if (!lifecycle)
47
57
  return;
48
58
  try {
49
- const feature = await contextRepository.findById(contextFeatureId);
50
- if (!feature)
51
- return;
52
- await contextRepository.update({
53
- ...feature,
54
- lifecycle,
55
- updatedAt: new Date(),
56
- });
59
+ await contextUpdater.execute({ featureId: contextFeatureId, lifecycle });
57
60
  }
58
61
  catch {
59
62
  // Swallow — lifecycle update failure is non-fatal
@@ -0,0 +1,24 @@
1
+ /**
2
+ * CI Helper Utilities
3
+ *
4
+ * Shared helpers for the CI watch/fix loop and merge node:
5
+ * - extractRunId: parse GitHub Actions run ID from URL
6
+ * - handleCiTerminalFailure: update feature repo on CI failure
7
+ * - buildCiExhaustedError: structured error for exhausted/timed-out loops
8
+ */
9
+ import type { IFeatureRepository } from '../../../../../../application/ports/output/repositories/feature-repository.interface.js';
10
+ import { type CiFixRecord } from '../../../../../../domain/generated/output.js';
11
+ /**
12
+ * Extract the numeric GitHub Actions run ID from a run URL.
13
+ * Example: https://github.com/org/repo/actions/runs/12345 → "12345"
14
+ */
15
+ export declare function extractRunId(runUrl: string): string | undefined;
16
+ /**
17
+ * Update the feature repository to mark CI as failed before throwing.
18
+ */
19
+ export declare function handleCiTerminalFailure(feature: Awaited<ReturnType<Pick<IFeatureRepository, 'findById'>['findById']>>, prUrl: string | null, prNumber: number | null, featureRepository: Pick<IFeatureRepository, 'findById' | 'update'>, messages: string[]): Promise<void>;
20
+ /**
21
+ * Build a structured error message describing the CI fix loop outcome.
22
+ */
23
+ export declare function buildCiExhaustedError(attempts: number, history: CiFixRecord[], reason: 'exhausted' | 'timeout'): Error;
24
+ //# sourceMappingURL=ci-helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ci-helpers.d.ts","sourceRoot":"","sources":["../../../../../../../../../../packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/ci-helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yEAAyE,CAAC;AAClH,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAEpF;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAG/D;AAED;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,kBAAkB,EAAE,UAAU,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAC9E,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,iBAAiB,EAAE,IAAI,CAAC,kBAAkB,EAAE,UAAU,GAAG,QAAQ,CAAC,EAClE,QAAQ,EAAE,MAAM,EAAE,GACjB,OAAO,CAAC,IAAI,CAAC,CAef;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,WAAW,EAAE,EACtB,MAAM,EAAE,WAAW,GAAG,SAAS,GAC9B,KAAK,CAUP"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * CI Helper Utilities
3
+ *
4
+ * Shared helpers for the CI watch/fix loop and merge node:
5
+ * - extractRunId: parse GitHub Actions run ID from URL
6
+ * - handleCiTerminalFailure: update feature repo on CI failure
7
+ * - buildCiExhaustedError: structured error for exhausted/timed-out loops
8
+ */
9
+ import { PrStatus, CiStatus } from '../../../../../../domain/generated/output.js';
10
+ /**
11
+ * Extract the numeric GitHub Actions run ID from a run URL.
12
+ * Example: https://github.com/org/repo/actions/runs/12345 → "12345"
13
+ */
14
+ export function extractRunId(runUrl) {
15
+ const match = runUrl.match(/\/runs\/(\d+)/);
16
+ return match ? match[1] : undefined;
17
+ }
18
+ /**
19
+ * Update the feature repository to mark CI as failed before throwing.
20
+ */
21
+ export async function handleCiTerminalFailure(feature, prUrl, prNumber, featureRepository, messages) {
22
+ if (feature && prUrl && prNumber) {
23
+ await featureRepository.update({
24
+ ...feature,
25
+ lifecycle: feature.lifecycle,
26
+ pr: {
27
+ url: prUrl,
28
+ number: prNumber,
29
+ status: PrStatus.Open,
30
+ ciStatus: CiStatus.Failure,
31
+ },
32
+ updatedAt: new Date(),
33
+ });
34
+ }
35
+ messages.push(`[merge] CI watch/fix loop failed — feature halted`);
36
+ }
37
+ /**
38
+ * Build a structured error message describing the CI fix loop outcome.
39
+ */
40
+ export function buildCiExhaustedError(attempts, history, reason) {
41
+ const reasonStr = reason === 'timeout' ? 'CI watch timed out' : `all ${attempts} fix attempt(s) exhausted`;
42
+ const historyStr = history
43
+ .map((r) => ` - Attempt ${r.attempt}: ${r.outcome} (started ${r.startedAt})`)
44
+ .join('\n');
45
+ const detail = historyStr ? `\nAttempt history:\n${historyStr}` : '';
46
+ return new Error(`CI watch/fix loop failed — ${reasonStr}.${detail}\nReview CI logs and fix manually.`);
47
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * CI Watch/Fix Loop
3
+ *
4
+ * After a push, watches CI status and attempts automatic fixes when CI fails.
5
+ * Respects configurable max attempts, timeout, and log size from settings.
6
+ */
7
+ import type { IAgentExecutor } from '../../../../../../application/ports/output/agents/agent-executor.interface.js';
8
+ import type { IFeatureRepository } from '../../../../../../application/ports/output/repositories/feature-repository.interface.js';
9
+ import type { IGitPrService } from '../../../../../../application/ports/output/services/git-pr-service.interface.js';
10
+ import { CiStatus, type CiFixRecord } from '../../../../../../domain/generated/output.js';
11
+ import type { NodeLogger } from '../node-helpers.js';
12
+ import type { AgentExecutionOptions } from '../../../../../../application/ports/output/agents/agent-executor.interface.js';
13
+ export interface CiWatchFixDeps {
14
+ executor: IAgentExecutor;
15
+ gitPrService: IGitPrService;
16
+ featureRepository: Pick<IFeatureRepository, 'findById' | 'update'>;
17
+ }
18
+ export interface CiWatchFixParams {
19
+ cwd: string;
20
+ branch: string;
21
+ options: AgentExecutionOptions;
22
+ feature: Awaited<ReturnType<Pick<IFeatureRepository, 'findById'>['findById']>>;
23
+ prUrl: string | null;
24
+ prNumber: number | null;
25
+ existingAttempts: number;
26
+ messages: string[];
27
+ log: NodeLogger;
28
+ }
29
+ export type CiFixStatusValue = 'idle' | 'watching' | 'fixing' | 'success' | 'exhausted' | 'timeout';
30
+ export interface CiWatchFixResult {
31
+ ciStatus: CiStatus;
32
+ ciFixAttempts: number;
33
+ ciFixHistory: CiFixRecord[];
34
+ ciFixStatus: CiFixStatusValue;
35
+ }
36
+ /**
37
+ * Run the CI watch/fix loop. Watches for the initial CI result, then
38
+ * iteratively attempts fixes up to the configured maximum.
39
+ *
40
+ * Throws on timeout or exhausted attempts (after updating feature state).
41
+ */
42
+ export declare function runCiWatchFixLoop(deps: CiWatchFixDeps, params: CiWatchFixParams): Promise<CiWatchFixResult>;
43
+ //# sourceMappingURL=ci-watch-fix-loop.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ci-watch-fix-loop.d.ts","sourceRoot":"","sources":["../../../../../../../../../../packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/ci-watch-fix-loop.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+DAA+D,CAAC;AACpG,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yEAAyE,CAAC;AAClH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iEAAiE,CAAC;AAKrG,OAAO,EAAE,QAAQ,EAAE,KAAK,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC1E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,+DAA+D,CAAC;AAK3G,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,cAAc,CAAC;IACzB,YAAY,EAAE,aAAa,CAAC;IAC5B,iBAAiB,EAAE,IAAI,CAAC,kBAAkB,EAAE,UAAU,GAAG,QAAQ,CAAC,CAAC;CACpE;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,qBAAqB,CAAC;IAC/B,OAAO,EAAE,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,kBAAkB,EAAE,UAAU,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IAC/E,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,GAAG,EAAE,UAAU,CAAC;CACjB;AAED,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,UAAU,GAAG,QAAQ,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC;AAEpG,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,QAAQ,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,WAAW,EAAE,gBAAgB,CAAC;CAC/B;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,gBAAgB,CAAC,CAiH3B"}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * CI Watch/Fix Loop
3
+ *
4
+ * After a push, watches CI status and attempts automatic fixes when CI fails.
5
+ * Respects configurable max attempts, timeout, and log size from settings.
6
+ */
7
+ import { GitPrError, GitPrErrorCode, } from '../../../../../../application/ports/output/services/git-pr-service.interface.js';
8
+ import { CiStatus } from '../../../../../../domain/generated/output.js';
9
+ import { retryExecute } from '../node-helpers.js';
10
+ import { buildCiWatchFixPrompt } from '../prompts/merge-prompts.js';
11
+ import { extractRunId, handleCiTerminalFailure, buildCiExhaustedError } from './ci-helpers.js';
12
+ import { getSettings } from '../../../../../services/settings.service.js';
13
+ /**
14
+ * Run the CI watch/fix loop. Watches for the initial CI result, then
15
+ * iteratively attempts fixes up to the configured maximum.
16
+ *
17
+ * Throws on timeout or exhausted attempts (after updating feature state).
18
+ */
19
+ export async function runCiWatchFixLoop(deps, params) {
20
+ const { executor, gitPrService } = deps;
21
+ const { cwd, branch, options, feature, prUrl, prNumber, messages, log } = params;
22
+ const settings = getSettings();
23
+ const maxAttempts = settings.workflow?.ciMaxFixAttempts ?? 3;
24
+ const timeoutMs = settings.workflow?.ciWatchTimeoutMs ?? 600_000;
25
+ const logMaxChars = settings.workflow?.ciLogMaxChars ?? 50_000;
26
+ let ciFixAttempts = params.existingAttempts;
27
+ const ciFixHistory = [];
28
+ let ciFixStatus;
29
+ log.info(`Starting CI watch (maxAttempts=${maxAttempts}, timeout=${timeoutMs}ms)`);
30
+ // Check if any CI run exists for this branch
31
+ const initialCiStatus = await gitPrService.getCiStatus(cwd, branch);
32
+ if (!initialCiStatus.runUrl) {
33
+ log.info('No CI run detected after push — skipping CI watch');
34
+ return { ciStatus: CiStatus.Success, ciFixAttempts, ciFixHistory, ciFixStatus: 'idle' };
35
+ }
36
+ let runUrl = initialCiStatus.runUrl;
37
+ // Initial CI watch
38
+ let watchResult;
39
+ try {
40
+ watchResult = await gitPrService.watchCi(cwd, branch, timeoutMs);
41
+ }
42
+ catch (err) {
43
+ if (err instanceof GitPrError && err.code === GitPrErrorCode.CI_TIMEOUT) {
44
+ log.info('Initial CI watch timed out');
45
+ ciFixHistory.push({
46
+ attempt: ciFixAttempts + 1,
47
+ startedAt: new Date().toISOString(),
48
+ failureSummary: 'CI watch timed out',
49
+ outcome: 'timeout',
50
+ });
51
+ await handleCiTerminalFailure(feature, prUrl, prNumber, deps.featureRepository, messages);
52
+ throw buildCiExhaustedError(ciFixAttempts + 1, ciFixHistory, 'timeout');
53
+ }
54
+ throw err;
55
+ }
56
+ if (watchResult.status === 'success') {
57
+ log.info('CI passed on first watch');
58
+ return { ciStatus: CiStatus.Success, ciFixAttempts, ciFixHistory, ciFixStatus: 'success' };
59
+ }
60
+ // CI failed — enter fix loop
61
+ while (true) {
62
+ // Fail-fast: check attempt count BEFORE invoking executor (NFR-3)
63
+ if (ciFixAttempts >= maxAttempts) {
64
+ log.info(`CI fix loop exhausted after ${ciFixAttempts} attempt(s)`);
65
+ ciFixStatus = 'exhausted';
66
+ break;
67
+ }
68
+ // Fetch failure logs
69
+ const runId = extractRunId(runUrl) ?? '';
70
+ const failureLogs = await gitPrService.getFailureLogs(runId, branch, logMaxChars);
71
+ const startedAt = new Date().toISOString();
72
+ log.info(`CI fix attempt ${ciFixAttempts + 1}/${maxAttempts} for run ${runId}`);
73
+ // Invoke fix executor
74
+ const fixPrompt = buildCiWatchFixPrompt(failureLogs, ciFixAttempts + 1, maxAttempts, branch);
75
+ await retryExecute(executor, fixPrompt, options, { logger: log });
76
+ ciFixAttempts++;
77
+ // Get updated run URL (new run triggered by push in fix)
78
+ const updatedCiStatus = await gitPrService.getCiStatus(cwd, branch);
79
+ if (updatedCiStatus.runUrl)
80
+ runUrl = updatedCiStatus.runUrl;
81
+ // Watch CI after fix
82
+ let fixWatchResult;
83
+ try {
84
+ fixWatchResult = await gitPrService.watchCi(cwd, branch, timeoutMs);
85
+ }
86
+ catch (err) {
87
+ if (err instanceof GitPrError && err.code === GitPrErrorCode.CI_TIMEOUT) {
88
+ log.info(`CI watch timed out during fix attempt ${ciFixAttempts}`);
89
+ ciFixHistory.push({
90
+ attempt: ciFixAttempts,
91
+ startedAt,
92
+ failureSummary: failureLogs.slice(0, 500),
93
+ outcome: 'timeout',
94
+ });
95
+ ciFixStatus = 'timeout';
96
+ break;
97
+ }
98
+ throw err;
99
+ }
100
+ const outcome = fixWatchResult.status === 'success' ? 'fixed' : 'failed';
101
+ ciFixHistory.push({
102
+ attempt: ciFixAttempts,
103
+ startedAt,
104
+ failureSummary: failureLogs.slice(0, 500),
105
+ outcome,
106
+ });
107
+ if (fixWatchResult.status === 'success') {
108
+ log.info(`CI passed after fix attempt ${ciFixAttempts}`);
109
+ ciFixStatus = 'success';
110
+ return { ciStatus: CiStatus.Success, ciFixAttempts, ciFixHistory, ciFixStatus };
111
+ }
112
+ log.info(`CI still failing after fix attempt ${ciFixAttempts}`);
113
+ messages.push(`[merge] CI fix attempt ${ciFixAttempts}/${maxAttempts} — still failing`);
114
+ }
115
+ // Handle terminal failure states
116
+ await handleCiTerminalFailure(feature, prUrl, prNumber, deps.featureRepository, messages);
117
+ throw buildCiExhaustedError(ciFixAttempts, ciFixHistory, ciFixStatus);
118
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-output-parser.d.ts","sourceRoot":"","sources":["../../../../../../../../../../packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/merge-output-parser.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI7D;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAI/D"}
@@ -6,11 +6,16 @@
6
6
  * 2. Merge/squash (via agent executor, after approval gate)
7
7
  *
8
8
  * Uses interrupt() for the merge approval gate when allowMerge=false.
9
+ *
10
+ * Sub-modules:
11
+ * - ci-watch-fix-loop.ts: CI watch + automatic fix loop
12
+ * - ci-helpers.ts: shared CI utility functions
13
+ * - merge-output-parser.ts: structured data extraction from agent output
9
14
  */
10
- import type { IAgentExecutor } from '../../../../../application/ports/output/agents/agent-executor.interface.js';
11
- import type { FeatureAgentState } from '../state.js';
12
- import type { IFeatureRepository } from '../../../../../application/ports/output/repositories/feature-repository.interface.js';
13
- import type { DiffSummary } from '../../../../../application/ports/output/services/git-pr-service.interface.js';
15
+ import type { IAgentExecutor } from '../../../../../../application/ports/output/agents/agent-executor.interface.js';
16
+ import type { FeatureAgentState } from '../../state.js';
17
+ import type { IFeatureRepository } from '../../../../../../application/ports/output/repositories/feature-repository.interface.js';
18
+ import type { DiffSummary, IGitPrService } from '../../../../../../application/ports/output/services/git-pr-service.interface.js';
14
19
  export interface MergeNodeDeps {
15
20
  executor: IAgentExecutor;
16
21
  getDiffSummary: (cwd: string, baseBranch: string) => Promise<DiffSummary>;
@@ -22,6 +27,7 @@ export interface MergeNodeDeps {
22
27
  * Returns true if baseBranch contains all commits from featureBranch.
23
28
  */
24
29
  verifyMerge: (cwd: string, featureBranch: string, baseBranch: string) => Promise<boolean>;
30
+ gitPrService: IGitPrService;
25
31
  }
26
32
  /**
27
33
  * Factory that creates the merge node function.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge.node.d.ts","sourceRoot":"","sources":["../../../../../../../../../../packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/merge.node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+DAA+D,CAAC;AACpG,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yEAAyE,CAAC;AAClH,OAAO,KAAK,EACV,WAAW,EACX,aAAa,EACd,MAAM,iEAAiE,CAAC;AAsBzE,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,cAAc,CAAC;IACzB,cAAc,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;IAC1E,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7C,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACnD,iBAAiB,EAAE,IAAI,CAAC,kBAAkB,EAAE,UAAU,GAAG,QAAQ,CAAC,CAAC;IACnE;;;OAGG;IACH,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1F,YAAY,EAAE,aAAa,CAAC;CAC7B;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,aAAa,IAGnC,OAAO,iBAAiB,KAAG,OAAO,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,CA6M7E"}
@@ -6,15 +6,21 @@
6
6
  * 2. Merge/squash (via agent executor, after approval gate)
7
7
  *
8
8
  * Uses interrupt() for the merge approval gate when allowMerge=false.
9
+ *
10
+ * Sub-modules:
11
+ * - ci-watch-fix-loop.ts: CI watch + automatic fix loop
12
+ * - ci-helpers.ts: shared CI utility functions
13
+ * - merge-output-parser.ts: structured data extraction from agent output
9
14
  */
10
15
  import { interrupt, isGraphBubbleUp } from '@langchain/langgraph';
11
- import { SdlcLifecycle, PrStatus } from '../../../../../domain/generated/output.js';
12
- import { createNodeLogger, shouldInterrupt, retryExecute, buildExecutorOptions, getCompletedPhases, clearCompletedPhase, markPhaseComplete, } from './node-helpers.js';
13
- import { reportNodeStart } from '../heartbeat.js';
14
- import { recordPhaseStart, recordPhaseEnd, recordApprovalWaitStart, } from '../phase-timing-context.js';
15
- import { updateNodeLifecycle } from '../lifecycle-context.js';
16
- import { buildCommitPushPrPrompt, buildMergeSquashPrompt } from './prompts/merge-prompts.js';
16
+ import { SdlcLifecycle, PrStatus } from '../../../../../../domain/generated/output.js';
17
+ import { createNodeLogger, shouldInterrupt, retryExecute, buildExecutorOptions, getCompletedPhases, clearCompletedPhase, markPhaseComplete, } from '../node-helpers.js';
18
+ import { reportNodeStart } from '../../heartbeat.js';
19
+ import { recordPhaseStart, recordPhaseEnd, recordApprovalWaitStart, } from '../../phase-timing-context.js';
20
+ import { updateNodeLifecycle } from '../../lifecycle-context.js';
21
+ import { buildCommitPushPrPrompt, buildMergeSquashPrompt } from '../prompts/merge-prompts.js';
17
22
  import { parseCommitHash, parsePrUrl } from './merge-output-parser.js';
23
+ import { runCiWatchFixLoop } from './ci-watch-fix-loop.js';
18
24
  /**
19
25
  * Factory that creates the merge node function.
20
26
  *
@@ -28,9 +34,6 @@ export function createMergeNode(deps) {
28
34
  reportNodeStart('merge');
29
35
  await updateNodeLifecycle('merge');
30
36
  // --- Rejection detection on resume (same pattern as executeNode) ---
31
- // On resume after interrupt, check if merge was already completed.
32
- // Rejection: clear phase, schedule re-execution via conditional edge.
33
- // Approval: fall through to post-interrupt work (lifecycle update, optional merge/squash).
34
37
  const completedPhases = getCompletedPhases(state.specDir);
35
38
  const isResumeAfterInterrupt = completedPhases.includes('merge') && shouldInterrupt('merge', state.approvalGates);
36
39
  if (isResumeAfterInterrupt && state._approvalAction === 'rejected') {
@@ -47,12 +50,10 @@ export function createMergeNode(deps) {
47
50
  }
48
51
  const messages = [];
49
52
  const startTime = Date.now();
50
- // Record merge phase timing
51
53
  const mergeTimingId = await recordPhaseStart('merge');
52
54
  try {
53
55
  const { executor } = deps;
54
56
  const cwd = state.worktreePath;
55
- // Resolve branch name from feature
56
57
  const feature = await deps.featureRepository.findById(state.featureId);
57
58
  const branch = feature?.branch ?? `feat/${state.featureId}`;
58
59
  const baseBranch = await deps.getDefaultBranch(cwd);
@@ -60,7 +61,10 @@ export function createMergeNode(deps) {
60
61
  let commitHash = state.commitHash;
61
62
  let prUrl = state.prUrl;
62
63
  let prNumber = state.prNumber;
63
- const ciStatus = state.ciStatus;
64
+ let ciStatus = state.ciStatus;
65
+ let ciFixAttempts = state.ciFixAttempts ?? 0;
66
+ let ciFixHistory = state.ciFixHistory ?? [];
67
+ let ciFixStatus = state.ciFixStatus ?? 'idle';
64
68
  // --- Check for git remote (needed by both agent calls) ---
65
69
  const remoteAvailable = await deps.hasRemote(cwd);
66
70
  // --- Agent Call 1: Commit + Push + PR (skip on approval resume) ---
@@ -68,14 +72,12 @@ export function createMergeNode(deps) {
68
72
  if (!remoteAvailable) {
69
73
  log.info('No git remote configured — skipping push and PR, will merge locally');
70
74
  }
71
- // Override push/openPr when no remote is available
72
75
  const effectiveState = remoteAvailable ? state : { ...state, push: false, openPr: false };
73
76
  log.info('Agent call 1: commit + push + PR');
74
77
  const commitPushPrPrompt = buildCommitPushPrPrompt(effectiveState, branch, baseBranch);
75
78
  const commitResult = await retryExecute(executor, commitPushPrPrompt, options, {
76
79
  logger: log,
77
80
  });
78
- // Parse structured data from agent output
79
81
  commitHash = parseCommitHash(commitResult.result) ?? state.commitHash;
80
82
  messages.push(`[merge] Agent completed commit/push/PR operations`);
81
83
  if (effectiveState.openPr) {
@@ -86,6 +88,28 @@ export function createMergeNode(deps) {
86
88
  messages.push(`[merge] PR created: ${prUrl}`);
87
89
  }
88
90
  }
91
+ // --- CI watch/fix loop (when push or openPr is enabled) ---
92
+ if (effectiveState.push || effectiveState.openPr) {
93
+ const ciResult = await runCiWatchFixLoop({
94
+ executor,
95
+ gitPrService: deps.gitPrService,
96
+ featureRepository: deps.featureRepository,
97
+ }, {
98
+ cwd,
99
+ branch,
100
+ options,
101
+ feature,
102
+ prUrl,
103
+ prNumber,
104
+ existingAttempts: ciFixAttempts,
105
+ messages,
106
+ log,
107
+ });
108
+ ciStatus = ciResult.ciStatus;
109
+ ciFixAttempts = ciResult.ciFixAttempts;
110
+ ciFixHistory = ciResult.ciFixHistory;
111
+ ciFixStatus = ciResult.ciFixStatus;
112
+ }
89
113
  // --- Merge approval gate ---
90
114
  if (shouldInterrupt('merge', state.approvalGates)) {
91
115
  log.info('Interrupting for merge approval');
@@ -99,6 +123,7 @@ export function createMergeNode(deps) {
99
123
  diffSummary,
100
124
  prUrl,
101
125
  prNumber,
126
+ ciStatus,
102
127
  });
103
128
  }
104
129
  }
@@ -141,6 +166,8 @@ export function createMergeNode(deps) {
141
166
  status: merged ? PrStatus.Merged : PrStatus.Open,
142
167
  ...(commitHash ? { commitHash } : {}),
143
168
  ...(ciStatus ? { ciStatus: ciStatus } : {}),
169
+ ...(ciFixAttempts > 0 ? { ciFixAttempts } : {}),
170
+ ...(ciFixHistory.length > 0 ? { ciFixHistory } : {}),
144
171
  },
145
172
  }
146
173
  : {}),
@@ -159,21 +186,21 @@ export function createMergeNode(deps) {
159
186
  prUrl,
160
187
  prNumber,
161
188
  ciStatus,
189
+ ciFixAttempts,
190
+ ciFixHistory,
191
+ ciFixStatus,
162
192
  _approvalAction: null,
163
193
  _rejectionFeedback: null,
164
194
  _needsReexecution: false,
165
195
  };
166
196
  }
167
197
  catch (err) {
168
- // Re-throw LangGraph control-flow exceptions (interrupt, etc.)
169
198
  if (isGraphBubbleUp(err))
170
199
  throw err;
171
200
  const message = err instanceof Error ? err.message : String(err);
172
201
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
173
202
  log.error(`Merge failed: ${message} (${elapsed}s)`);
174
- // Record phase end even on failure so timing shows duration, not "running"
175
203
  await recordPhaseEnd(mergeTimingId, Date.now() - startTime);
176
- // Re-throw so LangGraph does NOT checkpoint this node as completed.
177
204
  throw err;
178
205
  }
179
206
  };
@@ -13,6 +13,18 @@ import type { FeatureAgentState } from '../../state.js';
13
13
  * on state.push and state.openPr flags.
14
14
  */
15
15
  export declare function buildCommitPushPrPrompt(state: FeatureAgentState, branch: string, baseBranch: string): string;
16
+ /**
17
+ * Build a prompt for the CI watch/fix agent call.
18
+ *
19
+ * Instructs the executor to diagnose CI failure logs, apply a targeted fix,
20
+ * commit with the prescribed conventional message format, and push to the branch.
21
+ *
22
+ * @param failureLogs - Truncated CI failure log output
23
+ * @param attemptNumber - 1-based current attempt number
24
+ * @param maxAttempts - Maximum allowed attempts
25
+ * @param branch - Feature branch name to push to after fixing
26
+ */
27
+ export declare function buildCiWatchFixPrompt(failureLogs: string, attemptNumber: number, maxAttempts: number, branch: string): string;
16
28
  /**
17
29
  * Build a prompt for the merge/squash agent call.
18
30
  *
@@ -1 +1 @@
1
- {"version":3,"file":"merge-prompts.d.ts","sourceRoot":"","sources":["../../../../../../../../../../packages/core/src/infrastructure/services/agents/feature-agent/nodes/prompts/merge-prompts.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAqCxD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,iBAAiB,EACxB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,GACjB,MAAM,CAyDR;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,iBAAiB,EACxB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,SAAS,UAAQ,GAChB,MAAM,CAkER"}
1
+ {"version":3,"file":"merge-prompts.d.ts","sourceRoot":"","sources":["../../../../../../../../../../packages/core/src/infrastructure/services/agents/feature-agent/nodes/prompts/merge-prompts.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAqCxD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,iBAAiB,EACxB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,GACjB,MAAM,CAyDR;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,MAAM,EACrB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,GACb,MAAM,CA0BR;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,iBAAiB,EACxB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,SAAS,UAAQ,GAChB,MAAM,CAkER"}
@@ -97,6 +97,44 @@ ${steps.join('\n')}
97
97
  - Do NOT amend existing commits
98
98
  - If there are no changes to commit, skip the commit step and report that no changes were found`;
99
99
  }
100
+ /**
101
+ * Build a prompt for the CI watch/fix agent call.
102
+ *
103
+ * Instructs the executor to diagnose CI failure logs, apply a targeted fix,
104
+ * commit with the prescribed conventional message format, and push to the branch.
105
+ *
106
+ * @param failureLogs - Truncated CI failure log output
107
+ * @param attemptNumber - 1-based current attempt number
108
+ * @param maxAttempts - Maximum allowed attempts
109
+ * @param branch - Feature branch name to push to after fixing
110
+ */
111
+ export function buildCiWatchFixPrompt(failureLogs, attemptNumber, maxAttempts, branch) {
112
+ return `You are fixing a CI failure on branch \`${branch}\` (attempt ${attemptNumber}/${maxAttempts}).
113
+
114
+ ## CI Failure Logs
115
+
116
+ The following logs were retrieved from the failed GitHub Actions run:
117
+
118
+ \`\`\`
119
+ ${failureLogs}
120
+ \`\`\`
121
+
122
+ ## Instructions
123
+
124
+ 1. Analyze the CI failure logs above to diagnose the root cause
125
+ 2. Apply a targeted fix to resolve the failure — change only what is necessary
126
+ 3. Stage all changes: \`git add -A\`
127
+ 4. Commit with this exact conventional commit message format:
128
+ \`fix(ci): attempt ${attemptNumber}/${maxAttempts} — <short description of what you fixed>\`
129
+ 5. Push the fix to the branch: \`git push origin ${branch}\`
130
+
131
+ ## Constraints
132
+
133
+ - Fix ONLY the issue(s) causing the CI failure — do not refactor unrelated code
134
+ - The commit message MUST start with \`fix(ci): attempt ${attemptNumber}/${maxAttempts} — \`
135
+ - Do NOT create a new branch — push directly to \`${branch}\`
136
+ - If the failure is unclear, make your best diagnosis and explain your reasoning in the commit message`;
137
+ }
100
138
  /**
101
139
  * Build a prompt for the merge/squash agent call.
102
140
  *
@@ -1,4 +1,4 @@
1
- import type { ApprovalGates } from '../../../../domain/generated/output.js';
1
+ import type { ApprovalGates, CiFixRecord } from '../../../../domain/generated/output.js';
2
2
  /**
3
3
  * State annotation for the feature-agent graph.
4
4
  *
@@ -47,6 +47,9 @@ export declare const FeatureAgentAnnotation: import("@langchain/langgraph").Anno
47
47
  ciStatus: import("@langchain/langgraph").BinaryOperatorAggregate<string | null, string | null>;
48
48
  push: import("@langchain/langgraph").BinaryOperatorAggregate<boolean, boolean>;
49
49
  openPr: import("@langchain/langgraph").BinaryOperatorAggregate<boolean, boolean>;
50
+ ciFixAttempts: import("@langchain/langgraph").BinaryOperatorAggregate<number, number>;
51
+ ciFixHistory: import("@langchain/langgraph").BinaryOperatorAggregate<CiFixRecord[], CiFixRecord[]>;
52
+ ciFixStatus: import("@langchain/langgraph").BinaryOperatorAggregate<"success" | "timeout" | "idle" | "watching" | "fixing" | "exhausted", "success" | "timeout" | "idle" | "watching" | "fixing" | "exhausted">;
50
53
  }>;
51
54
  export type FeatureAgentState = typeof FeatureAgentAnnotation.State;
52
55
  //# sourceMappingURL=state.d.ts.map